Building KithFund: A full-stack cooperative fund platform with TanStack Start, Hono, and Expo
How I built a cooperative sinking-fund platform with a shared API for web and mobile — money handling, audit logs, and payment proof workflows.
- TypeScript
- TanStack
- React Native
- PostgreSQL
- Fintech
Member-run cooperative sinking funds are usually tracked in spreadsheets. Treasurers copy formulas between tabs, members text screenshots of GCash transfers, and nobody trusts the numbers at cycle end. I built KithFund to replace that — a full-stack platform where treasurers manage contributions, loans, and distributions from a web workspace, and members pay dues and check balances from a native mobile app.
Why spreadsheets fail
A sinking fund has real money rules: biweekly contributions per "head," internal loans with guarantors, interest distribution at cycle close, and payment proofs that need treasurer approval. Spreadsheets work until they don't — one broken formula, one overwritten cell, one member who can't open Excel on their phone.
The product goal was simple: one source of truth that works for the treasurer at a laptop and the member on the go.
Architecture
KithFund is a pnpm monorepo with three clients sharing one backend:
Web (TanStack Start) ──┐
├──► Hono API (/api/v1) ──► PostgreSQL (Drizzle)
Mobile (Expo) ─────────┘ │
└──► R2/S3 blobs · Resend · Expo Push
- Web — TanStack Start with SSR, role-based routing (admin vs member), contribution matrix, ledger, and charts.
- API — Hono with Zod validation, ~35 route modules, bearer tokens for mobile and session auth for web.
- Mobile — Expo 54 with Expo Router, push notifications, deep links, and payment proof uploads.
- Shared packages —
@kithfund/sharedfor types/formatting and@kithfund/api-clientso web and mobile never duplicate API logic.
Deploying on Railway (app + Postgres) keeps ops simple for a cooperative-scale product.
Hard problems worth getting right
Money is not a float
Every monetary column uses numeric(14,2) in PostgreSQL. JavaScript numbers stay out of the ledger. Distribution snapshots freeze at cycle close so historical reports don't shift when someone edits a row later.
RBAC that matches the domain
Admins see the full fund; members see only their heads, loans, and ledger entries. Route handlers check role and fund scope before querying — the API client types help, but the server is the authority.
Payment proof approval
The flow members actually use:
- Member pays via GCash or bank transfer offline.
- Mobile app uploads a screenshot to object storage (presigned URL).
- Payment enters a pending queue.
- Treasurer reviews in the admin inbox and approves or rejects.
- Approved payments post to the ledger and update contribution status.
Idempotency keys prevent double-posting if the member retries the upload.
Audit hash-chain
Every admin action appends to an audit log with a hash chain — each entry includes the previous hash so tampering breaks the chain. For a fund where trust is the product, this matters more than a slick animation.
Mobile without a WebView
The Expo app is not a wrapped website. It uses the same typed ApiClient as the web app, stores bearer tokens securely, registers device tokens for push, and handles kithfund:// deep links for invite tokens.
Treasurers can approve pending payments from mobile too — separate admin tab navigation, same API.
What I'd do differently
- More integration tests around payment approval and cycle close — the financial logic has unit tests, but end-to-end coverage would speed up refactors.
- Earlier screenshot fixtures for store submission — capturing real device frames takes longer than you expect.
- Stricter feature flags per fund from day one — easier than migrating settings later.
See it live
I'm open to full-time engineering roles and selective contract work — especially teams building React, TypeScript, or mobile products with real domain complexity.