You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4.0 KiB
4.0 KiB
HQ Console — Postgres Switch (D15 executed → D19)
STATUS: IN BUILD, started 2026-07-17. The dedicated slice queued after the D18 go-live cluster and before the real APEX cutover — so production data is born in Postgres and nothing is ever migrated.
1. Goal & why now
D15 locked Postgres as the production engine; the repo has run on better-sqlite3
throughout development, with the portable-repo pattern maintained specifically so this
switch stays a contained task. Doing it before the one-shot 300-client import means:
empty Postgres → seed → import APEX CSVs straight in. No SQLite→Postgres data
migration, ever.
2. Locked decisions
| # | Decision | Why |
|---|---|---|
| D19.1 | Driver: pg (node-postgres) with a Pool |
Boring, ubiquitous, first-class TS types |
| D19.2 | Async Db interface (query/get/all/run/transaction) with TWO implementations: better-sqlite3 (dev/tests, wraps sync in async) and pg |
There is no sync Postgres client for Node; the whole repo layer goes async/await once, mechanically. SQLite stays for fast :memory: tests — the dual-engine suite proves the portable-SQL rule |
| D19.3 | Paise columns: bigint in Postgres (INTEGER unchanged in SQLite) |
int4 overflows at ₹2.15 crore in paise; aggregate SUMs cross that immediately. pg returns bigint as string → parse at the adapter edge, still integer paise in the domain |
| D19.4 | Dates stay TEXT ISO (YYYY-MM-DD / ISO timestamps) |
Already the house convention; portable, index-friendly, zero timezone surprises |
| D19.5 | JSON payloads (document.payload, client.contacts, …): jsonb in Postgres, TEXT in SQLite; repos keep JSON.stringify/parse at the edge |
jsonb gets us future indexing for free without changing repo code |
| D19.6 | Migrations: numbered SQL files + a tiny schema_migrations runner (per engine schema file) |
Replaces the PRAGMA-guard migrate() + rebuildTable gymnastics; Postgres does real ALTER TABLE |
| D19.7 | Placeholders: repos keep ?; the pg adapter rewrites ? → $n |
One SQL dialect in the codebase; the adapter owns the difference |
| D19.8 | Engine differences behind the adapter: INSERT OR IGNORE → ON CONFLICT DO NOTHING, nested db.transaction → savepoints, sqlite_master → information_schema |
Repo code stays engine-blind |
| D19.9 | Config: DATABASE_URL env selects Postgres; absent → SQLite as today |
Dev machines keep working with zero setup; prod sets one variable |
3. Build order (each phase lands with the full suite green)
- Async refactor, SQLite still underneath — introduce the
Dbinterface, convert everyrepos-*.ts,api.ts, scheduler, server and test toasync/awaitwith the sqlite implementation behind it. Zero behavior change; this is ~70% of the diff and is deliberately not mixed with any engine change. - Postgres schema + migration runner —
schema.pg.sql(bigint paise, boolean, jsonb, real CHECKs) +schema_migrations. - pg adapter — Pool,
?→$n, ON CONFLICT mapping, savepoint-nested transactions, bigint parsing. - Dual-engine test run — full suite on
:memory:SQLite (default, fast) and on a real local Postgres viaTEST_PG=1+DATABASE_URL. Both green before landing. - Boot + smoke on local Postgres — server on
DATABASE_URL, real flows exercised (login → quote → issue → payment → PDF → share). - Prod later: RDS (Mumbai) when go-live infra is provisioned; same
DATABASE_URLmechanism, then cutover per DEPLOY-HQ.
4. Environment (this machine)
- PostgreSQL 17 installed locally as a Windows service (EDB installer, unattended);
superuser
postgres. App rolehq; databaseshq(dev runtime) andhq_test(suite). DBeaver connects tolocalhost:5432for inspection. DATABASE_URL=postgres://hq:hq_dev@localhost:5432/hq
5. Non-goals
RDS provisioning (separate infra step at go-live), pgbouncer/replicas, ORM adoption (the portable-repo pattern is the point), rewriting any business logic.