MVP Architecture
Overview
The architecture here is the result of genuine debate within the team across two fault lines:
PostgreSQL vs Firestore as the data store — one camp valued Firestore's simplicity, real-time subscriptions, and zero-ops scaling; the other valued PostgreSQL's ACID guarantees, relational integrity, rich querying, and auditability — all of which matter deeply for an accounting ledger.
REST vs event-driven — one camp preferred the familiarity and control of a REST API; the other wanted Firestore's effortless real-time data push to the UI with no polling or manual refresh logic.
Rather than choosing a side, the current design brings the best of both worlds together: Firestore delivers fast, effortless, real-time data to the UI — while PostgreSQL provides the strict ACID transactions, relational integrity, audit compliance, and schema control via Flyway that financial data demands. A Node.js sync service bridges the two, keeping Firestore current whenever PostgreSQL changes.
Data Flow
Write path — step by step
PostgreSQL — Source of Truth
All ledger data lives in Cloud SQL (PostgreSQL). Every write originates here.
Why PostgreSQL and not Firestore?
| PostgreSQL | Firestore | |
|---|---|---|
| Transactions | Full ACID transactions across multiple tables | Limited — no cross-document transactions without batching |
| Relational integrity | Foreign keys, constraints, cascades | No referential integrity |
| Querying | Full SQL — joins, aggregations, window functions | Limited — no joins, no aggregations server-side |
| Audit & compliance | Row-level history, triggers, CDC tooling | Difficult to audit; no native CDC |
| Double-entry accounting | Enforcing balanced debits and credits requires transactions | Cannot guarantee atomicity across a journal entry's lines |
| Schema control | Strict schema via Flyway migrations | Schemaless — easy to drift |
For an accounting ledger, correctness and consistency are non-negotiable. PostgreSQL gives us the transactional guarantees and query flexibility that Firestore cannot. Firestore serves as the real-time read layer — delivering live data to the UI with Firestore Security Rules enforcing tenant isolation, without any REST API in the middle.
Node.js Service
The Node.js service (in the node repo) runs two responsibilities:
REST API
Fastify REST endpoints receive all write requests from the UI. The API handles:
- Authentication via Firebase bearer token verification
- Business logic (e.g. validating journal entry balance)
- CRUD against PostgreSQL
The UI never writes directly to PostgreSQL or Firestore for ledger data.
Firestore Sync Service
A long-running listener watches for PostgreSQL change notifications and writes the updated records to Firestore, keeping the real-time read layer current.
How it works:
- A trigger on PostgreSQL fires
pg_notifyon everyINSERT,UPDATE, orDELETEto the ledger and IAM tables. - The Node sync service holds an open connection to PostgreSQL and listens for these notifications.
- On notification, it reads the changed row and writes it to the corresponding Firestore document.
- The UI's
onSnapshotlistener receives the Firestore update and re-renders automatically.
Near-term implementation: pg_notify with a pg.Client listener — simple, low-latency,
no additional infrastructure required.
As we approach MVP, we should consider upgrading to a proper Change Data Capture (CDC) pipeline for reliability and replay capability:
- Staying on Google Cloud SQL → Datastream feeds changes into Pub/Sub or BigQuery without requiring a persistent connection from Node.
- Moving off Google Cloud → Debezium streaming into Kafka gives a durable, replayable event log that multiple consumers can subscribe to.
Firestore — Real-time Read Layer
Firestore is a first-class read layer, not a temporary cache. The UI reads ledger data, chart
of accounts, and user profiles directly from Firestore via onSnapshot subscriptions.
- Firestore Security Rules enforce tenant isolation — a user can only read documents belonging to their own organisation. Cross-tenant data access is prevented at the database level.
- The UI uses
onSnapshotlisteners to receive live updates as the sync service writes changes. - Firestore is never written to by the UI directly for ledger data. All writes go through the Node REST API → PostgreSQL → sync trigger → Firestore.
UI
The React UI (in the ui repo):
- Reads ledger data from Firestore via
onSnapshotsubscriptions, giving users a live view of the ledger that updates whenever any user makes a change. - Writes all changes through the Node REST API. The UI never touches PostgreSQL directly.
- Authenticates via Firebase Auth. The bearer token is forwarded to the Node REST API on every request and verified server-side.