All posts

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/shared for types/formatting and @kithfund/api-client so 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:

  1. Member pays via GCash or bank transfer offline.
  2. Mobile app uploads a screenshot to object storage (presigned URL).
  3. Payment enters a pending queue.
  4. Treasurer reviews in the admin inbox and approves or rejects.
  5. 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.

Email me · Resume