Skip to content

Plan: Finances native rebuild in CreatorTrack (retire Flask + SQLite)

URL: https://mkdocs.justinsforge.com/memory/plans/finances-native-rebuild-2026-07-05/

Date: 2026-07-05 Approved spec: Full native rebuild. The finance module becomes a self-contained, extractable module inside CreatorTrack: own finance.* Postgres schema (workspace-keyed), own API routes, own components; auth + workspace id are the only touchpoints with the rest of the app. All ingest jobs (SimpleFIN sync, categorize rules, recurring detection, Fidelity holdings + Yahoo pricing, alerts, weekly snapshots, receipt reconcile) are rewritten in TypeScript writing Postgres directly, scheduled via systemd timers hitting token-protected internal routes (fail-loud notify). Every bridged write route goes native and lib/finance-bridge.ts is deleted. Cutover makes Postgres the source of truth; the Flask app, finance.db, and the mirror script retire (archived first). Then each of the 13 surfaces gets a live iteration pass with Justin on a dev slot; the current port stays usable throughout.

Out of scope: - Multi-user / product-ized finance features (portability boundary is enforced, extraction itself is a later project) - ERPNext business-tab changes (stays read-only ERPNext API) - Historical 2023-2025 workbook import - New surfaces beyond the existing 13 tabs (receipts stays; content defined during iteration passes)

Working style: branch feat/finances-native in /home/justinwieb/forge-suite, dev slot via the standard bootstrap, commit per task, merge to main ONCE at each phase boundary (per reference_creatortrack_parallel_dev). Dev DB = lifeos_dev_base clone; nothing touches prod Postgres until Phase C cutover.

Design language (confirmed 2026-07-05): surfaces are rebuilt in CreatorTrack's native design system: components/ui primitives + workspace shell patterns (as Habits/Tasks), Geist/Geist Mono, CreatorTrack tokens. The ported Flask skin (app/finances/finances.css, Outfit font, old Material markup) is DELETED during Phase D passes. Tooling and connections follow repo idioms: App Router routes, lib/db.ts + RLS, db/migrations, token-gate style matching /api/capture. Boundary amendment: finance may import shared UI primitives (components/ui) and platform glue (lib/auth, lib/db, lib/rls), nothing else; primitives copy over on any future extraction.

Phase A: module boundary + native ingest

Task 1: Declare the module boundary

  • Files: /home/justinwieb/forge-suite/FINANCES-APP-SPEC.md (new "Module boundary" section), /home/justinwieb/forge-suite/eslint.config.mjs
  • What: Document the extraction contract (finance code imports only lib/auth, lib/db, lib/rls; nothing outside app/finances|app/api/finance|components/finance|lib/finance* may import finance internals except the registry + FinanceApp). Add no-restricted-imports ESLint rules enforcing both directions.
  • Verification: cd /home/justinwieb/forge-suite && npx eslint app/finances components/finance --max-warnings 0
  • Commit: docs(finances): module boundary contract + eslint enforcement

Task 2: Job-state tables migration

  • Files: /home/justinwieb/forge-suite/db/migrations/<next>_finance_jobs.sql
  • What: Add finance.job_runs (job, started_at, ok, detail; observability replaces journalctl-only) and finance.alert_state (dedup keys + 90d prune, replaces alerts_state.json). No changes to existing finance.* tables.
  • Verification: psql -h 192.168.86.86 -U lifeos_admin -d lifeos_dev_base -c "\dt finance.*" | grep job_runs
  • Commit: feat(finances): job_runs + alert_state tables

Task 3: SimpleFIN client in TypeScript

  • Files: /home/justinwieb/forge-suite/lib/finance-simplefin.ts
  • What: Port forge_simplefin_client.py: access-URL basic auth from env SIMPLEFIN_ACCESS_URL, custom UA header (CDN 403s default agents), typed accounts/transactions response.
  • Verification: cd /home/justinwieb/forge-suite && npx tsx -e "import('./lib/finance-simplefin.ts').then(m=>m.fetchAccounts().then(a=>console.log(a.accounts.length)))" (with env sourced) prints 28
  • Commit: feat(finances): native SimpleFIN client

Task 4: Token-gated job runner route

  • Files: /home/justinwieb/forge-suite/app/api/finance/jobs/[job]/route.ts, /home/justinwieb/forge-suite/lib/finance-jobs.ts
  • What: POST route, bearer FINANCE_JOBS_TOKEN (added to ~/.forge-secrets/creatortrack-auth.env), dispatches to a job registry, records finance.job_runs, failures return 500 AND fire forge_notify.sh warning (fail-loud).
  • Verification: curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:<devport>/api/finance/jobs/sync returns 401 without token, 200 with
  • Commit: feat(finances): job runner route with token gate + job_runs logging

Task 5: Sync job native

  • Files: /home/justinwieb/forge-suite/lib/finance-jobs/sync.ts
  • What: Port forge_finance_sync.py: upsert accounts/transactions (idempotent on simplefin_txn_id), write balances, warn-notify when a run writes zero balances (the 06-09 freeze guard).
  • Verification: run job twice against dev DB; second run reports 0 new txns and identical counts (select count(*) from finance.transactions)
  • Commit: feat(finances): native SimpleFIN sync job

Task 6: Categorize engine native

  • Files: /home/justinwieb/forge-suite/lib/finance-jobs/categorize.ts
  • What: Port forge_finance_categorize.py (merchant/keyword rules, 16 categories, never clobbers user_category/user_edited).
  • Verification: on dev DB, job_runs row ok=true and spot-check: uncategorized count drops, a user-edited txn keeps its category
  • Commit: feat(finances): native categorize engine

Task 7: Recurring scan native

  • Files: /home/justinwieb/forge-suite/lib/finance-jobs/recurring.ts
  • What: Port forge_finance_recurring.py cadence detection + amount_cv fixed-vs-varies, preserving user overrides (nickname/type/fixed flags).
  • Verification: dev run leaves the 90 existing recurring rows' user fields untouched (select count(*) from finance.recurring where nickname is not null unchanged)
  • Commit: feat(finances): native recurring scan

Task 8: Holdings import + pricing native

  • Files: /home/justinwieb/forge-suite/lib/finance-jobs/holdings.ts
  • What: Port forge_finance_holdings.py --auto: watch /mnt/workspace/Forge/Fidelity Positions/, snapshot-replace per account label, archive to processed/, mark-to-market via Yahoo chart endpoint (SPAXX pinned $1).
  • Verification: drop the last processed CSV back in on dev; job imports it and select count(*) from finance.holdings = 23
  • Commit: feat(finances): native holdings import + pricing

Task 9: Alerts engine native

  • Files: /home/justinwieb/forge-suite/lib/finance-jobs/alerts.ts
  • What: Port forge_finance_alerts.py: bills due 24h, large txns (posted_at-guarded), budget 80/100% crossings, recurring price increases, low checking; dedup via finance.alert_state; one digest per run via forge_notify.sh info.
  • Verification: dev run twice; second run sends nothing (dedup) and alert_state rows exist
  • Commit: feat(finances): native alerts engine

Task 10: Snapshot + receipt-reconcile jobs native

  • Files: /home/justinwieb/forge-suite/lib/finance-jobs/snapshot.ts, /home/justinwieb/forge-suite/lib/finance-jobs/receipts.ts
  • What: Port forge_finance_snapshot.py (signed weekly net-worth contributions) and the receipt-reconcile timer job (FINDB- cheque matching).
  • Verification: dev snapshot writes rows for this week (select count(*) from finance.weekly_snapshots where week = date_trunc('week', now())::date)
  • Commit: feat(finances): native snapshot + receipt reconcile jobs

Phase B: native writes (bridge removal)

Task 11: Transaction writes native

  • Files: /home/justinwieb/forge-suite/app/api/finance/transactions/route.ts, callers in /home/justinwieb/forge-suite/components/finance/ (TxnEdit + bulk edit forms)
  • What: Per-txn edit (category/scope/note/tax-deductible/ignore/flag) + bulk edit write finance.transactions directly, setting user_category/user_edited sticky flags.
  • Verification: edit a txn on dev slot UI; row updates in dev PG; old Flask untouched
  • Commit: feat(finances): native transaction writes

Task 12: Recurring + categorize-rules writes native

  • Files: /home/justinwieb/forge-suite/app/api/finance/recurring/route.ts, /home/justinwieb/forge-suite/app/api/finance/rules/route.ts, components/finance/RecActions.tsx, components/finance/CzFileForm.tsx
  • What: Confirm/ignore/edit recurring and the rules engine apply path write PG natively.
  • Verification: confirm a recurring item on dev slot; finance.recurring row flips
  • Commit: feat(finances): native recurring + rules writes

Task 13: Budgets, manual values, sync trigger native

  • Files: /home/justinwieb/forge-suite/app/api/finance/budgets/route.ts, .../manual-values/route.ts, components/finance/SyncButton.tsx
  • What: Budget targets/autoset and manual asset values write PG; SyncButton posts to /api/finance/jobs/sync.
  • Verification: set a budget target on dev slot; row in finance.budgets updates; Sync button returns ok
  • Commit: feat(finances): native budget + manual value writes, sync trigger

Task 14: Icon subsystem native

  • Files: /home/justinwieb/forge-suite/app/api/finance/icons/route.ts, components/finance/IconPicker.tsx
  • What: Port forge_finances_ui_icons.py set/upload to PG finance.merchant_icons + file store under the module's own dir. ALWAYS key by _icon_key, never merchant_norm (reference_finances_icon_persistence).
  • Verification: set an icon on dev slot; survives a categorize job re-run
  • Commit: feat(finances): native icon subsystem

Task 15: Delete the bridge

  • Files: delete /home/justinwieb/forge-suite/lib/finance-bridge.ts, /home/justinwieb/forge-suite/components/finance/ComingSoon.tsx; sweep remaining imports
  • What: No finance code path may reach :8096 anymore.
  • Verification: grep -rn "finance-bridge\|8096" /home/justinwieb/forge-suite/app /home/justinwieb/forge-suite/components /home/justinwieb/forge-suite/lib returns nothing; npx tsc --noEmit clean
  • Commit: feat(finances)!: remove Flask bridge, all writes native

Phase C: cutover (Postgres becomes truth)

Task 16: Archive before retiring

  • Files: none (ops)
  • What: pg_dump -n finance of prod lifeos + copy forge/data/finance/finance.db to /home/justinwieb/forge-backups/finances-cutover-$(date +%Y%m%d)/. Security rule 5: nothing retires until both archives verified.
  • Verification: ls -la /home/justinwieb/forge-backups/finances-cutover-*/ shows both files, sizes nonzero
  • Commit: none (ops step, log to daily)

Task 16.5: Repoint the ERPNext books engine (DISCOVERED DEPENDENCY)

  • Files: /home/justinwieb/forge/scripts/forge_erpnext_daily_post.py (+ its DB layer)
  • What: The ERPNext daily books post reads finance.db (SQLite) and stamps Journal Entries with cheque_no = FINDB-<sqlite txn id>. Before SQLite retires it must read the finance.* Postgres tables instead, keying new JEs on FINDB-<pg id> while old rows resolve via legacy_findb_id (the receipts job already coalesces both). NEVER re-run its historical --apply (reference_erpnext_daily_post).
  • Verification: one supervised dry-run against PG produces the same pending-post set as the SQLite run on the same day
  • Commit: feat(finance-books): daily post reads finance.* Postgres

Task 17: Timers flip

  • Files: /home/justinwieb/forge/scripts/systemd/forge-creatortrack-finance-sync.{service,timer} (+ snapshot, receipt variants), installed to ~/.config/systemd/user/
  • What: New timers curl the token-gated job routes on :3070 (sync 3x daily, snapshot Sun 18:00, receipts 08:00). Run one supervised prod sync. Then disable forge-finance-sync.timer, forge-finance-snapshot.timer, forge-finance-receipt-reconcile.timer and the migrate ExecStartPost.
  • Verification: systemctl --user list-timers | grep creatortrack-finance; psql ... "select * from finance.job_runs order by started_at desc limit 3" all ok=true
  • Commit: feat(finances): prod timers on native job routes, legacy timers off

Task 18: Retire Flask

  • Files: none (ops)
  • What: systemctl --user disable --now forge-finances.service; leave finances.justinsforge.com CF ingress as a 302 redirect to https://app.creatortrack.ai/finances (reversible, bookmarks keep working).
  • Verification: curl -s -o /dev/null -w '%{http_code} %{redirect_url}' https://finances.justinsforge.com shows the redirect; CreatorTrack finance pages load fresh data next sync
  • Commit: none (ops step, log to daily)

Task 19: Repoint homepage tile

  • Files: /home/justinwieb/forge/sites/justinsforge.com/landing/index.html (line ~781)
  • What: Finances tile URL -> https://app.creatortrack.ai/finances; deploy via /deploy-site.
  • Verification: curl -s https://justinsforge.com | grep -c "app.creatortrack.ai/finances" >= 1
  • Commit: fix(landing): finances tile -> CreatorTrack

Phase D: surface iteration passes (with Justin, dev slot, one commit each)

Each task: rebuild/redesign the surface live with Justin driving, in CreatorTrack's design system (components/ui, Geist, workspace shell; NOT the ported Flask skin); Verification = Justin approves on the dev slot URL; Commit = feat(finances): <surface> iteration pass. Files per task: app/finances/<surface>/page.tsx + components/finance/* as needed. Task 32 (Alerts, the final pass) additionally deletes app/finances/finances.css + the Outfit import, since by then no surface references them (tracked here so it never dangles).

  • Task 20: Dashboard
  • Task 21: Net Worth
  • Task 22: Investments
  • Task 23: Spending
  • Task 24: Budget
  • Task 25: Business
  • Task 26: Recurring
  • Task 27: Cash Flow
  • Task 28: Accounts
  • Task 29: Transactions
  • Task 30: Categorize
  • Task 31: Receipts
  • Task 32: Alerts

Final

Task 33: Register and document

  • Files: /home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md, /home/justinwieb/forge/memory/general/reference_finances_app.md (rewrite: CreatorTrack native = canonical; Flask retired), /home/justinwieb/forge/memory/general/reference_creatortrack_finance_module.md (new: module boundary, job routes, timers, extraction contract)
  • What: Update the index, retire stale "FINANCES still canonical" notes, run bash /home/justinwieb/forge/scripts/forge_eval_run.sh.
  • Verification: eval run green; grep -c "finance-bridge" /home/justinwieb/forge/memory/general/reference_finances_app.md = 0 outside the history note
  • Commit: docs(finances): native module registered, Flask retirement documented