CreatorTrack multi-tenant FIN connect (SimpleFIN)¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_creatortrack_finance_fin_connect/
Makes Finances a real multi-tenant app: every user connects THEIR OWN SimpleFIN Bridge (users self-provision at bridge.simplefin.org and pay SimpleFIN directly; the platform has zero per-user cost). Branch feat/fin-connect-p0 (NOT merged, NOT deployed). Spec: fin-connect build spec. The finance data layer + job pipeline were already workspace-clean; this fills the four hardcoded-to-Justin gaps.
What's built (P0-P3, each committed + verified)¶
- P0 credential store.
finance.simplefin_connections(one per workspace,UNIQUE(workspace_id), workspace-membership RLS) holds the access URL ENCRYPTED via the existing AES-256-GCMAUTH_SECRETkeystore (lib/llm/keystoreencryptKey/decryptKey, reused, no new secret). Store liblib/finance-simplefin-store.ts(store/get/meta/status/markSynced/delete). Clientlib/finance-simplefin.tsnow takes a per-connection URL (fetchAccounts({accessUrl}),accessParts(url)) with global-secret fallback, andclaimSetupToken()ports the Python one-time token claim into TS.syncJobreads the per-workspace URL (getAccessUrl), stampslast_sync_at. One-timescripts/fin-connect-adopt-global-url.mts <ws>moves Justin's global URL into a #67 connection on deploy. - P1 self-serve connect.
POST /finances/connect/token(workspace owner/admin): claim -> validate via a balances probe ->storeConnection-> initial sync. UIcomponents/finance/BankConnectPanel.tsx(bridge.simplefin.org -> paste token -> connect), wired into the Finances sidebar. - P2 per-workspace sync fan-out.
finance.active_sync_targets()(SECURITY DEFINER; enumerates active connections + their owner, bypassing RLS).runJobForWorkspace+runJobAllConnectionsfan a job over all connections (+ the legacy global-secret owner until adopted); failures isolate per-workspace; a SimpleFIN 403 flips the connection toneeds_reauth./api/finance/jobs/[job]fans out by default,?workspace=<id>for one. - P3 gate flip. Every finance read/edit door (14 files) now gates on
financeAccessAllowed(lib/finance-access.ts, the single policy chokepoint = any authenticated member) instead of theisPersonalAppsOwneremail lock. RLS +resolveFinanceWorkspacescope each user to their own workspace's data; a stranger sees empty finances until they connect. Fitness/Calendar keep the email gate (they read Justin's server data). Legacyfinances/connect(finance.db migration) stays owner-only. The Krystal co-owner share still works.
Verified (dev clone, real asUser/HTTP-less paths)¶
Encrypt->store->decrypt exact roundtrip; fetchAccounts vs SimpleFIN public demo = 3 accounts / 60 txns; claim surfaces the one-time 403 + rejects invalid tokens; adopt stores an active connection; fan-out enumerated the target and synced the demo connection (3 accts / 172 txns, ok:true, isolated); a non-member sees 0 of #67's rows. tsc + webpack build clean.
Deploy checklist (when Justin approves)¶
- Merge
feat/fin-connect-p0; deploy (runs the 3 migrations). - Run
scripts/fin-connect-adopt-global-url.mts 67on prod to move Justin's global URL into a #67 connection. - Post-adopt cleanup (own commit): delete the global-secret fallback in
finance-simplefin.ts+ theFINANCE_CONNECT_PASSPHRASElegacy path. - Live end-to-end needs a real one-time setup token from the interactive bridge (can't automate).
Open product decisions (NOT blockers)¶
Bank-sync is open + self-serve (users bring their own SimpleFIN key; no paid tier, no per-user cost). If ever wanted, restrict in ONE place: financeAccessAllowed (e.g. workspace-owner-only, invite gate).
[Claude Code, 2026-07-07]