Skip to content

Finances -> lifeos Migration Plan (first real Suite module)

URL: https://mkdocs.justinsforge.com/memory/plans/finances-to-lifeos-migration-2026-06-17/

Date: 2026-06-17. Author: [Claude Code], with Justin. Status: plan, pending one decision (FIN Bridge transition). Reads alongside the Suite plan: memory/plans/workspace-suite-architecture-and-rebuild-plan-2026-06-17.md.

Goal: make finances the first real module of The Suite, by migrating the live finance data into lifeos Postgres (workspace-scoped + RLS) and rebuilding the UI in forge-suite, WITHOUT breaking the live money pipeline. Be thoughtful: money data + a scheduled ingest.

1. Current state (verified 2026-06-17)

  • Data store: SQLite data/finance/finance.db. Tables (rows): accounts (62), transactions (8611), balances (1132), budgets (37), recurring (86), holdings (23), manual_values (37), merchant_icons (63), nw_history_grid (6040), weekly_snapshots (644), etf_constituents (827), receipts (0).
  • Idempotency keys: transactions UNIQUE(source, external_id); SimpleFIN ids simplefin_txn_id (txns) and simplefin_id (accounts).
  • Live pipeline (timers, all write finance.db today):
  • forge-finance-sync.timer -> forge_finance_ingest.py = the FIN Bridge (SimpleFIN).
  • forge-finance-snapshot.timer -> balances/weekly snapshots.
  • forge-erpnext-post.timer -> posts to ERPNext (downstream, unaffected by where we read).
  • forge-finance-receipt-reconcile.timer.
  • UI: Flask forge-finances.service (port 8096), 14 feature blueprints: dashboard, transactions, accounts, budget, spending, cashflow, investments, networth, recurring, categorize, alerts, business, receipts, icons.

2. Target: a finance schema in lifeos

A new finance schema in lifeos Postgres (CT 109), workspace-scoped + RLS via core.apply_workspace_rls(), in Justin's workspace (multi-tenant-ready for future creators). Mirror finance.db table-for-table, adding workspace_id to every table, keeping the same natural keys for idempotency:

finance.db table lifeos finance.
notes
accounts accounts + workspace_id; keep simplefin_id unique-per-ws
transactions transactions + workspace_id; UNIQUE(workspace_id, source, external_id); keep simplefin_txn_id
balances balances + workspace_id
budgets budgets + workspace_id (composite key category+scope+ws)
recurring recurring + workspace_id
holdings holdings + workspace_id
manual_values manual_values + workspace_id
merchant_icons merchant_icons + workspace_id
nw_history_grid nw_history_grid + workspace_id
weekly_snapshots weekly_snapshots + workspace_id
etf_constituents etf_constituents reference data; ws-scope optional (could be global)
receipts receipts + workspace_id (0 rows today)

DDL lives as a numbered SQL migration (the Python migration runner owns lifeos DDL; Drizzle in forge-suite is a typed read/write CLIENT, it does not own the schema). Add Drizzle table defs in forge-suite/db/schema.ts (a finance pgSchema) after the migration lands.

3. One-time backfill (SQLite -> Postgres)

A Python script (reuse the existing finance env + psycopg admin) that reads finance.db and bulk -inserts into lifeos finance.*, stamping Justin's workspace_id. Idempotent: ON CONFLICT on the natural keys, so it can run repeatedly and re-runs are no-ops. Verify row counts match per table and spot-check sums (total transactions, latest balances) before trusting it.

4. DECIDED (2026-06-17): full clean-slate migration, lifeos is the single source of truth

Per Justin's standing rule ([[feedback_suite_clean_slate_rebuild]]): full migration, no mirror, no clinging. lifeos Postgres becomes the one source of truth. The SimpleFIN FIN Bridge and all finance writers are repointed to write Postgres directly; finance.db and the Flask finances app are retired once the Suite module is verified. A SQLite->Postgres mirror was explicitly rejected as bloat (two stores, a sync layer).

Anti-bloat in the schema: migrate only the source data (accounts, transactions, balances, holdings, manual_values, budgets, recurring, merchant_icons). Treat derived/cache tables (nw_history_grid, weekly_snapshots, etf_constituents) as things the Python jobs regenerate against Postgres, not historical baggage to import. Reference the old code for logic; do not copy its architecture.

5. Build order

  1. Write the finance schema migration (DDL) + apply to lifeos. Non-destructive (new schema).
  2. Backfill script: finance.db -> lifeos finance.*; verify counts/sums.
  3. Mirror step (C1): wire into the sync/snapshot timers so Postgres stays current.
  4. Add Drizzle finance defs + a finance data layer in forge-suite (all reads under RLS).
  5. Build the module UI, dashboard first (the highest-value view), then transactions, accounts, then the rest of the 14 areas incrementally. Each replaces its placeholder in the renderer registry; finance node type -> FinancesApp.
  6. Once trusted: C2 repoint, retire SQLite + mirror.

6. Guardrails

  • Never double-count: all inserts idempotent on natural keys; verify before/after.
  • Do not break the live pipeline: C1 leaves it untouched; C2 only after the Suite is trusted.
  • RLS + workspace scoping from day one, so finances is multi-tenant-ready.
  • ERPNext posting reads its own source; unaffected by where the Suite reads.

7. Decision: RESOLVED

Full clean-slate migration (C2). lifeos single source of truth; FIN Bridge + writers repointed to Postgres; finance.db + Flask app retired after verification. No mirror.

[Claude Code]