Plan: CreatorTrack Finances per-workspace scoping¶
URL: https://mkdocs.justinsforge.com/memory/plans/creatortrack-finances-per-workspace-scoping-2026-07-08/
Date: 2026-07-08
Approved spec: Make CreatorTrack Finances per-workspace. Today all finance data (345 merchant_icons + accounts/transactions/budgets/balances) lives in workspace 67 (Personal) and surfaces identically in all 6 of Justin's workspaces because the finance read layer is pure-RLS and never consults the active-workspace cookie. Scope every read in lib/finance.ts and its API routes to the active workspace, switch writes from resolveFinanceWorkspace (always 67) to the active workspace, and keep RLS as the cross-tenant backstop. ws67 keeps all existing data as Personal finance; brand workspaces (Gus The Bass ws85, etc.) start empty until data is created/connected there. Justin's user id=1, memberships 67/74/77/78/82/85.
Chosen mechanism: A Postgres session GUC (app.finance_ws) set once inside asUser, read by a shared SQL fragment ws_filter(), instead of threading an activeWorkspaceId param through all 28 read signatures. Minimizes blast radius, stays idempotent, and RLS remains untouched underneath.
Out of scope:
- The generic icon-picker library (lib/icon-library.ts, data/workspace/icons/) stays a shared personal palette.
- Finance sharing / co-owner UX (feat/finances-shared-krystal, not deployed) beyond preserving its RLS backstop.
- Moving/migrating any existing finance rows between workspaces.
- SimpleFIN per-workspace connect flow (already shipped by fin-connect).
Pre-flight¶
- Dev slot via
scripts/forge_creatortrack_dev_base.sh(worktree + dedicated clone DB + dev-auth), branchfeat/finances-per-workspace. Runforge_workspace_migrate.pyafter bootstrap (base clone lags merged migrations). Commit early/often; merge ONCE via integrator; teardown slot after. - Confirm baseline read-only:
select workspace_id, count(*) from finance.merchant_icons group by 1;on the clone should show all rows in one ws before changes.
Task list¶
Task 1: Active-workspace resolver for the finance layer¶
- Files:
lib/finance-ws.ts(new) - What: Add
resolveActiveFinanceWorkspace(id: Identity): Promise<number>=getActiveWorkspaceId(id)(fromlib/active-workspace.ts) if set AND the user is a member of it, else fall back toresolveFinanceWorkspace(id)(home store). Never throws when a cookie is stale; always returns a workspace the caller belongs to. - Verification:
cd /home/justinwieb/creatortrack && npx tsx -e "import('./lib/finance-ws.ts').then(m=>console.log(typeof m.resolveActiveFinanceWorkspace))"printsfunction. - Commit:
feat(finance): active-workspace resolver with home-store fallback
Task 2: Set the finance workspace GUC inside asUser¶
- Files:
lib/rls.ts,lib/finance-ws.ts - What: Add an opt-in
asFinanceUser(id, fn)wrapper (or anasUseroption) that, after stampingapp.user_id, resolves the active finance workspace and runsselect set_config('app.finance_ws', <ws>, true)on the same tx. Expose afinanceWsFilterSQL helper returningsql\workspace_id = current_setting('app.finance_ws')::bigint``. RLS is unchanged and still runs underneath. - Verification:
npx tsx scripts/_smoke_finance_ws.ts(temp smoke, deleted after) sets a fake active ws and assertscurrent_setting('app.finance_ws')matches inside the tx. - Commit:
feat(finance): asFinanceUser sets app.finance_ws GUC + shared ws filter
Task 3: Scope the icon + account + balance reads¶
- Files:
lib/finance.ts - What: Route
getMerchantIconMap,listAccounts,getAccountDetail,getAccountSummary,getNetWorthSections,getNwGrid,getLastSyncthroughasFinanceUserand addand ${financeWsFilter}to their queries. - Verification: On dev slot as Justin with active ws=85 (Gus):
curl -fsS <slot>/api/finance/accountsreturns[]; with active ws=67 returns the real accounts. - Commit:
feat(finance): scope icon/account/balance reads to active workspace
Task 4: Scope the transaction reads¶
- Files:
lib/finance.ts - What: Same treatment for
queryTransactions,listTransactions,getRecentTransactions,getTransactionDetail,getCategorizeQueue,getReceipts,receiptImagePath. - Verification:
curl -fsS <slot>/api/finance/transactionsreturns empty on ws85, full list on ws67.tsc --noEmitclean. - Commit:
feat(finance): scope transaction/receipt reads to active workspace
Task 5: Scope budgets, recurring, spend/income/pnl analytics¶
- Files:
lib/finance.ts - What: Same for
getFinanceOverview,getBudgetData,listBudgets,listRecurring,getRecurringSections,getRecurringDetail,getSpendOverview,getMonthFlows,getSpendInsights,getIncomeInsights,getBizPnl,getInvestments,getSymbolPosition,getAlerts. - Verification:
curl -fsS <slot>/api/finance/budgetsand/recurringempty on ws85, populated on ws67. Overview endpoint 200s on both.tsc --noEmitclean. - Commit:
feat(finance): scope budget/recurring/analytics reads to active workspace
Task 6: Point finance writes at the active workspace¶
- Files:
lib/finance-icons.ts,lib/finance-logos.ts - What:
setMerchantIcon,setMerchantIconAuto,setMerchantIconAutoExact(and any manual account/txn create in the routes) resolve the insertworkspace_idviaresolveActiveFinanceWorkspaceinstead ofresolveFinanceWorkspace/order by workspace_id limit 1. On-conflict keys stay(workspace_id, merchant_key); upsert stays idempotent. - Verification: On dev slot with active ws=85, set a merchant logo via the UI/API, then
select workspace_id from finance.merchant_icons where merchant_key=<k>returns 85; the same key under ws67 is untouched. - Commit:
feat(finance): finance writes target the active workspace
Task 7: End-to-end browser verification on the dev slot¶
- Files: none (verification only)
- What: Playwright on the slot as Justin: Personal shows all data + logos; switch to Gus The Bass → Finances renders empty (no bleed, no orphaned logos); create an account + set a logo in Gus → shows only in Gus; switch back to Personal → unchanged. Confirm no "transactions with blank logos" state anywhere.
- Verification: Screenshot both workspaces' Finances; ws85 empty, ws67 full; the Gus-created row absent from ws67.
- Commit:
test(finance): verified per-workspace scoping end-to-end on dev slot
Task 8: Register and document¶
- Files:
MEMORY.md,memory/general/reference_creatortrack_finance_module.md(update the CANONICAL note to record per-workspace read scoping +app.finance_wsGUC),memory/general/reference_finances_icon_persistence.md(note workspace scoping) - What: Update the finance-module reference and the icon-persistence reference; add/refresh the MEMORY.md index line. Run the eval harness.
- Verification:
bash /home/justinwieb/forge/scripts/forge_eval_run.shpasses;grep -q app.finance_ws /home/justinwieb/forge/memory/general/reference_creatortrack_finance_module.md. - Commit:
docs(finance): register per-workspace scoping in memory + refs
Merge¶
Merge ONCE via integrator after all tasks verified on the slot; then teardown the dev slot (forge_creatortrack_agent_teardown.sh / let the reaper collect it).