The app lenders use to review loan requests, make offers, deploy capital, manage their wallet and withdrawals, and run their subscription.
Key features built
Loan request and offer flow:Infinite-scrolling feed of merchant loan requests with URL-driven status filters, sort headers, and date filtering. Make-offer flow with processing fee capped at the loan amount and limited to two decimals so an offer can never be malformed. Full state machine: confirm → approval-pending → acceptance → active loan with fee breakdown and transaction history.
Wallet with optimistic updates: Wallet balance ticks down immediately on withdrawal and rolls back if the request fails. The interface stays honest without waiting for a round trip.
Withdrawals with idempotency and fee previews: Each withdrawal carries a caller-supplied, namespaced idempotency key so a double-click can never produce a double debit. Before committing, the lender sees a provider-fee preview with a tooltip explaining what each fee covers.
Subscriptions: Plan selection, plan-change preview showing exact switch cost, renewal prompt on dashboard lapse, and an onboarding gate requiring a card before a paid plan activates.
Onboarding, settings, and account connections: Multi-step onboarding (business info, license upload, verification) in its own route group so each step enforces its own state. Settings covers account info, security, linked bank accounts via Mono Connect, and masked cards. Dashboard with capital metrics and Recharts visualisations.
Technical highlights
Idempotency keys threaded end-to-end through the proxy, namespaced per surface (withdrawals separate from everything else) so keys can't collide across features.
Optimistic UI via TanStack Query mutation lifecycle with automatic rollback on failure.
URL-driven infinite scroll with filters, sort, and date ranges via nuqs — shareable by copying the URL.
Virtualised transaction tables via TanStack Virtual for smooth performance regardless of history length.
Typed server actions via next-safe-action: type errors on contract changes instead of runtime crashes.
Built on shared monorepo packages: @opensylo/auth, @opensylo/storage, @opensylo/ui, @opensylo/observability.
Notable decisions
Optimistic updates only where rollback is clean: wallet balance gets it, loan-offer submission does not.
Processing fee capped at loan amount and two decimals enforced at the form level.
"Defaulted" renamed to "Late on payment" — a deliberate copy decision that shapes how lenders treat struggling merchants.
Onboarding steps in their own route groups so each step is independently reachable and enforces its own state.
Closing remarks
While building the lender app, it taught me that in a money-movement product, the hard problems aren't the visible features; they're the invisible guarantees.
A loan request feed is straightforward. Making sure a double-clicked withdrawal can never double-debit, that an optimistic balance always rolls back cleanly, that a processing fee can never be malformed. The lender app is where I spent the most time on things a user will, ideally, never notice.
Lead frontend engineer on OpenSylo's lender app — loan request feed, offer flow, wallet with optimistic updates, and idempotency-protected withdrawals.