A production-grade platform that runs a community's money in the open. It began for a single village of ~60 members and is built multi-tenant from day one, so the same deployment can serve apartment associations, societies, clubs, temple committees, institutions, and trusts — each with fully isolated data.
The core idea: nothing about the community's finances is hidden. Every member can see the balance, who has paid, every expense with its bill, event budgets, and donations — while personal member data stays private.
- Multi-tenant — every community's data is isolated structurally at the repository layer, not by per-endpoint discipline
- Secure auth — phone + password, 15-minute JWTs, rotating refresh tokens with theft detection, device tracking, account lockout
- Payments, three ways — Razorpay AutoPay subscriptions, one-time Razorpay pay links (pay any due from the app, alongside AutoPay), and manual cash/UPI with an approval queue — automatic PDF receipts for all
- Events with three funding modes — pick per event:
- From community balance — funded from the shared balance (may run negative on overspend)
- Split among members — a budget divided equally, auto-recalculated whenever membership changes
- Collect payment — a fixed amount from each chosen member, each with a Pay button; the member set and amount stay fixed
- Expenses & income — approval workflow, mandatory bills above a threshold, live budget tracking, donations and sponsorships
- Monthly dues tracking — see who has paid and who hasn't for any month, and a per-member ledger of every month paid or unpaid with receipts
- Members — CRUD, statuses, bulk CSV import, and group by family / household
- Reports & analytics — daily to yearly, immutable month-end snapshots, correction adjustments, and advanced exports (any dataset, custom range + filters) to PDF / Excel / CSV; viewable and downloadable from the mobile app too
- Admin from your phone — a role-gated admin tab: members, approvals, create/edit events, record payments/expenses/income, announcements, dues, and reports
- Transparency & search — global command-palette search; every financial record visible to members
- Append-only audit log — mutation is blocked at the schema level; records actor, before/after, IP, device
- Realtime notifications — Pusher channels and an in-app center; payment, event, budget, and announcement alerts
- Data management — a super-admin danger zone to clear data by section (payments, expenses, income, events, and more); audit logs and closed snapshots are never touched
- Release notes — an in-app What's New (WebView) and About page, backed by a public changelog
- Zero-cron scheduling — daily housekeeping (reminders, overdue marking, monthly close) self-triggers; no paid infrastructure
| Application | Audience | Stack |
|---|---|---|
| Admin dashboard + REST API | Super admins and admins | Next.js 15 (App Router), Tailwind v4, ShadCN-style UI, TanStack Query, Recharts |
| Member app (with admin tools) | Members, on their phones | Expo SDK 54 / React Native 0.81, expo-router, Material 3 (React Native Paper + NativeWind) |
Both are driven by one type-safe contract — the same Zod schemas, DTOs, enums, and money helpers — published from packages/shared.
The system is a Turborepo monorepo with two front-ends and one backend. The Next.js app is both the admin dashboard and the REST API; the Expo app is a pure client of that API. There is exactly one source of truth for every type, rule, and number.
┌──────────────────────────────────────────┐
│ packages/shared │
│ Zod schemas · DTOs · enums · money │
│ helpers (imported by BOTH front-ends │
│ and the API — one contract) │
└──────────────────────────────────────────┘
��� ▲
imports types & │ │ imports types &
validation │ │ validation
│ │
┌─────────────────────────────┴────────┐ ┌────────────┴──────────────────────┐
│ apps/mobile (Expo) │ │ apps/web (Next.js 15) │
│ React Native · Material 3 · Paper │ │ ┌───────────────────────────────┐ │
│ │ │ │ Admin dashboard (React) │ │
│ Screens (member + admin tools) │ │ │ TanStack Query · Recharts │ │
│ │ │ │ └───────────────┬───────────────┘ │
│ TanStack Query hooks │ │ │ same-origin fetch │
│ │ │ │ ▼ │
│ Axios client │ │ ┌───────────────────────────────┐ │
│ • access token in memory │ │ │ REST API /api/v1 (routes) │ │
│ • refresh token in SecureStore │ │ │ │ │ │
│ │ │ │ │ withApi() middleware: │ │
└────────┼──────────────────────────────┘ │ │ auth · RBAC · rate-limit · │ │
│ │ │ Zod validate · audit ctx │ │
│ HTTPS Bearer <accessToken> │ │ │ │ │
└───────────────────────────────────────┼─▶│ Controller → Service → │ │
REST /api/v1 │ │ Repository → Mongoose model │ │
│ └───────────────┬───────────────┘ │
┌───────────────────────┐ webhooks │ │ │
│ Razorpay │──────────────────────────▶│ (payment settlement pipeline) │
│ subscriptions/links │ payment_link.paid … │ ▼ │
└───────────────────────┘ └──────────────────┼──────────────────┘
│
┌───────────────┐ ┌───────────────┐ ┌──────────────────▼──────────────┐
│ Pusher │◀─────│ realtime │ │ MongoDB Atlas │
│ channels │ │ fan-out │ │ (tenant-scoped collections) │
└───────┬───────┘ └───────────────┘ └──────────────────────────────────┘
│ private-community-{id}
└──────────────▶ both front-ends subscribe for live updates
Cloudinary ◀── signed direct uploads (bills, photos) + receipt PDFs
Every request travels the same path; a layer never reaches around the one beneath it.
route.ts → service → repository → Mongoose model
route.ts— thin. Wrapped bywithApi(), which connects to Mongo, runs a fixed-window rate limiter, verifies the JWT, enforces role-based access, parses the body/query with a Zod schema, and seeds anAsyncLocalStoragerequest context (actor + device) for the audit trail. It then delegates and never touches a model directly.- Service layer — all business rules: the budget-split engine, the payment settlement pipeline, approval workflows, report aggregation, adjustments. Services are assembled by a small DI container so tests inject mocks with no framework.
- Repository layer — data access.
BaseRepositoryinjectscommunityIdinto every query and write, so multi-tenant isolation is structural: a developer literally cannot forget it. Cross-tenant super-admin operations use dedicated repositories. - Models — Mongoose schemas with compound
{ communityId, … }indexes. Two collections (audit log, closed-period snapshots) block updates and deletes at the schema level — they are append-only by construction.
Cross-cutting guarantees: a uniform { success, data, error, meta } envelope; a typed AppError hierarchy mapped to HTTP codes by one global handler; and all money stored as integer paise to eliminate floating-point drift.
A single request — say a member paying an event share from the phone — flows through the stack in one deterministic sequence:
1. Client mobile/web calls POST /api/v1/events/:id/pay
with Authorization: Bearer <accessToken>
│
2. withApi() connect DB (cached) → rate-limit (Mongo window)
middleware → verify JWT → check role → parse body/query with Zod
→ open AsyncLocalStorage request context (actor, ip, device)
│
3. Route handler resolves the service from the DI container, delegates
│
4. Service business rules run here:
• validate the member owes a share
• create/return a Razorpay payment link
• (on webhook) settlement pipeline:
income row → receipt PDF → split credit
→ notification → audit entry
│
5. Repository BaseRepository injects { communityId } into every
query/write — tenant isolation is automatic
│
6. Model Mongoose persists to the tenant-scoped collection
│
7. Side effects Cloudinary (receipt) · Pusher (realtime) · AuditLog
│
8. Response uniform { success, data, meta } envelope back to the
client; a thrown AppError maps to its HTTP status instead
Every mutation that changes money — manual approval on the web, a Pay link on mobile, or an AutoPay webhook — re-enters the same service-layer settlement pipeline at step 4, so the two front-ends can never disagree on the numbers.
apps/web is a single Next.js 15 project. Route handlers under app/api/v1/** are the backend above; the React pages under app/(dashboard)/** are the admin UI that calls them same-origin. Server state is TanStack Query; forms are React Hook Form + the shared Zod schemas; charts are Recharts. The dashboard is fully responsive with a mobile drawer.
apps/mobile (Expo) renders the member experience and admin tools in Material 3. It holds no business logic — it calls the same /api/v1. Its Axios client keeps the access token in memory and the refresh token in the device keychain (expo-secure-store); a response interceptor transparently rotates on 401 and retries. Members log in with phone + password, see their dues and event shares, and pay via Razorpay links opened in the browser. Admins get an extra, role-gated tab for members, approvals, events, and collections.
They never talk to each other — they converge on the API and the shared package.
- One contract.
packages/sharedexports every Zod schema, DTO type, enum, and money helper. The API validates requests with a schema; the web and mobile clients build requests against the same schema and render the same DTO types. A change to a rule or field is a single edit that both front-ends and the server pick up. - One API, two auth transports. The
/auth/*endpoints issue an access token plus a rotating refresh token. Web stores the refresh token in an httpOnlySameSite=Strictcookie; mobile stores it in secure storage and sends it in the request body. The same rotation-with-theft-detection logic serves both. - One realtime channel. After a payment settles or a budget recalculates, the server persists the change and fans out on the community's private Pusher channel. Both the dashboard and the mobile app subscribe and update live.
- Shared side effects. Whether a payment arrives from an admin's manual approval on the web, a member's Razorpay pay link on mobile, or an AutoPay webhook, it runs the same settlement pipeline — income row, receipt PDF, event-split credit, notification, audit entry — so the two apps always agree on the numbers.
Finance App/
├── apps/
│ ├── web/ Next.js 15 — admin dashboard + /api/v1 REST API
│ │ └── src/server/ models · repositories · services · middleware · lib
│ └── mobile/ Expo member app (Material 3, light mode)
├── packages/
│ ├── shared/ Zod schemas · DTOs · enums · money constants
│ └── config/ shared tsconfig presets
└── turbo.json Turborepo + npm workspaces
# 1. Install (Node >= 20)
npm install
# 2. Configure the API + dashboard
cp apps/web/.env.example apps/web/.env
# MONGODB_URI, JWT secrets (openssl rand -base64 48),
# FIELD_ENCRYPTION_KEY (openssl rand -hex 32), SEED_* values
# 3. Bootstrap your community + super admin (no demo data)
npm run seed
# 4. Run the dashboard
npm run dev # http://localhost:3000
# 5. Run the member app on your phone (Expo Go)
npm run mobile # scan the QR code| Integration | Enables | Env |
|---|---|---|
| Razorpay | AutoPay, online payments, refunds | RAZORPAY_* (or per-community in Settings) |
| Cloudinary | Bill/photo uploads, receipt PDFs | CLOUDINARY_* |
| Pusher | Realtime dashboard + notifications | PUSHER_*, NEXT_PUBLIC_PUSHER_* |
Everything else runs without them.
cd apps/mobile
npx expo prebuild --platform android --clean
echo "sdk.dir=$HOME/Library/Android/sdk" > android/local.properties
cd android && ./gradlew assembleRelease
# -> android/app/build/outputs/apk/release/app-release.apkOr use EAS for cloud builds and store distribution: npx eas-cli build --platform android --profile preview.
Versioned REST under /api/v1.
| Area | Endpoints |
|---|---|
| Auth | login · refresh (rotating) · logout(-all) · sessions · change-password |
| Members | CRUD · status · reset-password · bulk CSV import · ledger |
| Dues | who has paid / not paid a given month |
| Payments | list · manual entry · review · refund · pay (Razorpay link) · delete |
| Subscriptions | create/cancel/resume AutoPay · webhook (idempotent) |
| Events | CRUD · status · splits · split-history · pay |
| Expenses / Income / Documents | CRUD · approval · signed uploads |
| Reports | live · snapshots · adjustments · export · advanced export |
| Admin | audit-logs · communities · notifications · settings · search · cron · clear-data |
Full reference: docs/API.md.
npm run test # services + integration (mongodb-memory-server):
# budget-split math · token rotation & theft detection
# tenant isolation · audit immutability
npm run typecheck # strict TypeScript across the monorepo
npm run build # production build- API + dashboard — Vercel (import repo, root
apps/web, add env vars). Housekeeping self-triggers, so no cron configuration is needed. - Database — MongoDB Atlas (allow network access).
- Mobile — EAS build, then Play Store or internal APK.
- Access tokens live in memory only; refresh tokens in httpOnly
SameSite=Strictcookies (web) or secure storage (mobile), rotated every use - Aadhaar and Razorpay secrets are AES-256-GCM encrypted at rest; logs and audit diffs redact secrets
- HMAC-verified, idempotent webhooks · Mongo-backed rate limiting · account lockout
- Audit logs and closed-period snapshots are immutable at the schema level
Built by Nitheesh Rajendran — Founder, Setups Works
Website: https://setups.works · Email: info@setups.works

