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
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 is used purely as a read cache to deliver real-time UI updates — not as a data store.
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 UI's read cache 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 — Read Cache
Firestore holds a near-real-time copy of the ledger data that the UI subscribes to.
- 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.