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 outsideapp/finances|app/api/finance|components/finance|lib/finance*may import finance internals except the registry + FinanceApp). Addno-restricted-importsESLint 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) andfinance.alert_state(dedup keys + 90d prune, replacesalerts_state.json). No changes to existingfinance.*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 envSIMPLEFIN_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, recordsfinance.job_runs, failures return 500 AND fireforge_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/syncreturns 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 onsimplefin_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 clobbersuser_category/user_edited). - Verification: on dev DB,
job_runsrow 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.pycadence detection +amount_cvfixed-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 nullunchanged) - 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 toprocessed/, 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 viafinance.alert_state; one digest per run viaforge_notify.sh info. - Verification: dev run twice; second run sends nothing (dedup) and
alert_staterows 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.transactionsdirectly, settinguser_category/user_editedsticky 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.recurringrow 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.budgetsupdates; 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.pyset/upload to PGfinance.merchant_icons+ file store under the module's own dir. ALWAYS key by_icon_key, nevermerchant_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/libreturns nothing;npx tsc --noEmitclean - 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 financeof prod lifeos + copyforge/data/finance/finance.dbto/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 withcheque_no = FINDB-<sqlite txn id>. Before SQLite retires it must read thefinance.*Postgres tables instead, keying new JEs onFINDB-<pg id>while old rows resolve vialegacy_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.timerand 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; leavefinances.justinsforge.comCF ingress as a 302 redirect tohttps://app.creatortrack.ai/finances(reversible, bookmarks keep working). - Verification:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}' https://finances.justinsforge.comshows 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