
Bibit Backoffice.
Internal backoffice behind KYC verification and settlement for Indonesia's leading mutual fund app.
About the project.
Bibit serves millions of retail investors, and its KYC, settlement, and business teams run on this backoffice. I led the five-person frontend team behind it, reporting to the engineering manager and CTO: a six-month, zero-downtime migration from a legacy React SPA to Next.js at 99% uptime, atomic design that cut code duplication by ~40%, and SonarCloud quality gates held above 90%.
Challenges & decisions.
2. The Rewrite Story (Context)
Legacy app (2021–2023): A React SPA built with
create-react-app, manual CSS (no system), and React-Redux. It worked for 2 years, but as the ops team grew from 5 to 30+ users across Settlement, KYC, and external stakeholders, the cracks showed.
Why we had to rewrite:
Bundle size: 2.4MB main chunk. Dashboard took 6s to become interactive on office WiFi.
CSS chaos: 8,000+ lines of manual CSS, naming collisions, no design consistency between Settlement and KYC modules.
Redux boilerplate: Adding a single filter to the transaction table required touching 5 files (action, reducer, saga, types, component).
No SSR: Login page, report exports, and auth redirects were all client-side—janky and insecure.
Stakeholder expansion: External auditors and partner banks needed limited access. The old auth model couldn’t support modular, role-aware UI without massive hacks.
Decision: Incremental rewrite was riskier than a clean rebuild. We froze features for one quarter and rebuilt on Next.js 14.
3. Old vs. New Stack — Comparison Table
Table
Aspect | Old Stack | New Stack | Why We Migrated |
Framework | React 17 SPA (CRA) | Next.js 14 (App Router) | SSR for auth/login pages; nested layouts for Settlement vs KYC modules; API routes for lightweight BFF patterns |
State Management | React-Redux + Redux-Saga | Zustand | Redux was 70% boilerplate. Zustand = 1 file per store, no providers, no sagas, persisted auth state effortlessly |
Styling | Manual CSS files | Ant Design + Custom Tokens | AntD gave us 90% of complex UI (tables, forms, date-pickers) out of the box. Customized via
to match brand |
Data Fetching | Redux-Saga + Axios | Axios + Zustand stores | Centralized API layer with interceptors (token refresh, error handling). Zustand stores hold server state + UI state together |
Routing | React Router | Next.js App Router | File-based routing reduced config; route groups isolated
and
modules |
RBAC / UI Permissions | Hardcoded
| Permission matrix in Zustand +
| Roles grew from 3 to 8 (Settlement Team, Settlement Manager, KYC, KYC Manager, Auditor, Partner Bank, etc.). Hardcoding was unmaintainable |
Approval Flows | Hardcoded modal logic per page | Reusable
hook + dynamic config | Settlement and KYC have different hierarchies. Hook reads workflow config from API; no frontend deploy needed to change rules |
Build / Deploy | Manual Webpack | Vercel + CI/CD | Automatic previews per PR; zero-downtime deploys |
4. Architecture Diagrams
A. Component Architecture (Post-Rewrite)
plainCopy┌─ Next.js App Router │ ├─ (auth) group ── Login / Reset Password (RSC, minimal JS) │ ├─ (dashboard) group ── Layout Shell │ │ ├─ Sidebar ── Menu items filtered by Zustand auth role │ │ ├─ Header ── User badge, notifications │ │ └─ Module Routes │ │ ├─ /settlement │ │ │ ├─ TransactionTable (customized AntD Table) │ │ │ ├─ BulkActionBar ── <PermissionGate> │ │ │ └─ ApprovalDrawer ── powered by useApprovalFlow() │ │ └─ /kyc │ │ ├─ SubmissionQueue │ │ ├─ DocumentViewer │ │ └─ VerificationForm (customized AntD Form) │ └─ Shared Layer │ ├─ <PermissionGate> ── renders children or null based on matrix │ ├─ useAuthStore (Zustand) ── user, token, role permissions │ ├─ useSettlementStore (Zustand) ── local + server state │ └─ apiClient (Axios instance) ── interceptors, base URL, headers
B. Data Flow (Simplified)
plainCopyUser clicks "Approve" in Settlement Drawer ↓ <<PermissionGate> checks useAuthStore.roleMatrix ↓ useApprovalFlow() validates: "Can this user approve at this hierarchy level?" ↓ Optimistic UI update in useSettlementStore ↓ apiClient.post('/settlements/approve') → Axios interceptor attaches JWT ↓ Response → store updated → AntD notification → UI re-renders ↓ WebSocket broadcast updates other users' queues in real-time
5. Key Challenges & Solutions (Rewrite Edition)
Challenge 1: Migrating State from Redux to Zustand
Problem: 40+ Redux actions/reducers for settlement data alone. Migrating everything at once would stall the team for weeks.
Solution: Strangler Fig pattern. We built Zustand stores alongside Redux, then migrated one module at a time (Settlement first, then KYC). Used a compatibility layer so components could consume both during transition.
Outcome: Zero-downtime migration. Team shipped features throughout the rewrite.
Challenge 2: Replacing 8,000 Lines of Manual CSS with AntD Tokens
Problem: Legacy CSS had class name collisions (
table-wrapperdefined differently in 3 files). Stakeholders complained Settlement and KYC "felt like different apps."
Solution: Audited all CSS, mapped recurring values to AntD
ConfigProvidertokens (colorPrimary, borderRadius, fontSize). Built a thin wrapper component library (<<AppTable />,<AppForm />,<AppModal />) that enforced consistency.
Outcome: Deleted 6,200 lines of CSS. Both modules now share one visual language. New features take 30% less styling time.
Challenge 3: Role-Aware UI for 8 User Types
Problem: Settlement Team, Settlement Manager, KYC Officer, KYC Manager, Auditor, Partner Bank, Admin, Read-Only Viewer. Hardcoding
role === 'x'was no longer viable.
Solution: Backend serves a permission matrix at login. Frontend stores it in
useAuthStore. A single<PermissionGate required={['settlements.approve']}>component handles all UI gating. Approval buttons render dynamically based on hierarchy config.
Outcome: Adding "Partner Bank Viewer" required zero frontend code—just a backend config update.
Challenge 4: 50k-Row Transaction Table Performance
Problem: The old Redux + manual table re-rendered everything on every filter change. Browser froze.
Solution: AntD Table + server-side filtering/sorting + virtualized scroll for large datasets. Zustand store only holds viewport-visible data.
Outcome: Filter response: 4s → 280ms. Scroll: 60fps.
Overview
An internal web-based backoffice system supporting operational, compliance, and business teams in a regulated fintech environment. The platform needed to scale quickly while maintaining code quality, consistency, and ease of use for internal stakeholders.
Product & Technical Context
The existing codebase had grown organically, leading to duplicated UI patterns and slower development cycles. Improving maintainability and consistency became critical as new features and teams were added.
Key Challenges
Inconsistent UI patterns slowed development and increased maintenance overhead
Lack of standardized components made scaling the platform difficult
Limited visibility into code quality and technical debt
My Role & Responsibilities
Took ownership of frontend architecture and component standardization
Collaborated with multiple teams to align UI patterns with operational workflows
Mentored engineers on best practices and component-driven development
What I Did
Designed and implemented a scalable UI system based on Atomic Design principles
Built reusable component libraries to reduce duplication and improve consistency
Introduced SonarCloud for automated code quality analysis and technical debt tracking
Improved development workflows to support faster iteration and onboarding
Impact & Results
Reduced development time for new UI features by approximately 30%
Improved maintainability and reduced duplicated logic across the codebase
Increased overall code quality and reduced critical issues flagged by static analysis
Enabled faster onboarding and collaboration for new engineers
Built with.
A look inside.
