Architecture
PennyPlan v2 is two applications that talk only over HTTP, sharing one PostgreSQL database (owned by the API) and one Auth0 tenant.
Browser (PWA) ──HTTPS /v1──▶ pennyplan-api (NestJS) ──▶ PostgreSQL
IndexedDB outbox │
├──▶ Redis / BullMQ ──▶ workers (AI, notifications, digests)
├──▶ S3 (voice notes, receipts, exports)
└──▶ WhatsApp Cloud API (webhook in, replies out)The API is a modular monolith
One deployable, divided into modules with clear responsibilities. Services are split out only when load demands it.
| Module | Responsibility | Status |
|---|---|---|
health | Liveness and readiness | Built |
auth | Auth0 JWT validation, roles | Planned |
users | Profile, language, preferences, consent | Planned |
ledger | Entries, accounts, categories, dedupe, sync | Planned |
capture | Turns raw input into a draft entry | Planned |
budgets | Budgets and safe-to-spend | Planned |
insights | Summaries, weekly digest, assistant | Planned |
notifications | Scheduling, delivery, quiet hours | Planned |
whatsapp | Webhook, number linking, replies | Planned |
ai | Provider adapters, PII stripping, cost tracking | Planned |
Conventions built into the API today
- Everything is under
/v1. Breaking changes go to/v2or behind a flag. - One error shape: every error is
{ "code", "message", "details" }. Internal errors never leak details. - Validated configuration. The environment is checked once at boot and the process refuses to start if anything is missing or invalid.
- User-scoped data access. Services obtain a database client with
PrismaService.forUser(userId), which adds the user’s id to every query and refuses to touch any model that has not been explicitly marked as user-scoped. A forgotten model fails loudly instead of silently exposing data.
Offline-first by design
The web app writes entries locally first (IndexedDB) with client-generated UUIDs, then syncs through an outbox. The server de-duplicates by that UUID, so retries and reconnects never create duplicates.
Capture pipeline
Every input channel (text, voice, forwarded bank alert, receipt photo) ends in the same place: a draft entry the user confirms. Cheap, deterministic parsing always runs first; a model is used only when that fails or is unsure.
Last updated on