CreatorTrack finance sharing (co-ownership)¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_creatortrack_finance_sharing/
Lets a second user co-own a workspace's finance store WITHOUT joining that workspace, so their OWN workspace's Finances tab renders the shared dataset. Built for Justin sharing his Personal #67 finances with Krystal (branch feat/finances-shared-krystal, plan finances-shared-with-krystal-2026-07-07). She never joins #67, so every non-finance app's membership RLS still denies her by default.
Model¶
finance.shares(owner_workspace_id, shared_user_id, role='coowner')= realized grant.finance.share_invites(owner_workspace_id, email, role)= pending grant, keyed by email (claimed at signup/login).core.finance_shared_workspaces(user_id)(SECURITY DEFINER, STABLE): the owner workspace ids a user co-owns. Feeds the extendedworkspace_isolationRLS policy on all 19finance.*tables:workspace_id IN (memberships) OR workspace_id IN (finance_shared_workspaces). NoWITH CHECK, so USING doubles as the insert/update check, a co-owner reads AND writes the owner's rows.core.claim_finance_shares(user_id, email)(SECURITY DEFINER): upsertsfinance.sharesfrom matchingfinance.share_invitesand consumes the invite (so a later revoke stays revoked). Called best-effort at all 4 auth lifecycle sites (Google createUser+signIn inauth.ts, credentials signup+login inlib/auth-credentials.ts) next toroutePendingInvitesToConsent.
The two gates that had to change (both in finance ONLY)¶
Finances is a "personal-data app" gated TWO ways, both had to admit co-owners without touching the global gate (which many apps share):
1. Email owner gate isPersonalAppsOwner (lib/personal-apps.ts). Finance doors now use isPersonalAppsOwner(me) || await isFinanceCoOwner(me) (lib/finance-share.ts). Doors changed: read face components/apps/finance/FinanceApp.tsx; edit APIs transactions, recurring, manual-values, finances/categorize/apply, finances/icons/upload, finances/receipts/upload. STAY owner-only (touch the shared SimpleFIN credential): finances/connect, finances/sync.
2. RLS (above) so her reads/writes reach #67.
Write target¶
Reads need no workspace param (lib/finance.ts is pure-RLS). New-row inserts that used a caller-membership lookup now use resolveFinanceWorkspace(me) (lib/finance-share.ts): share-first, else own first-membership. Applied in budgets, setMerchantIcon, categorize/apply, receipts. Row/account-derived inserts (transactions update, manual_values, recurring excludes/dismissed) already yield the owner ws via RLS.
Verified (dev clone, real asUser path)¶
Co-owner (a user NOT in #67, given a finance.shares row) sees 7969 #67 txns (0 before the share); her category edit is visible to the owner; she sees 0 of #67's app.nodes (tasks/notes) but 14 of her own. Claim path: seeding an invite then claiming creates 1 share, marks co-owner true, consumes the invite.
Deploy / grant (per user)¶
Migrations 20260707T230232 (tables+RLS) and 20260707T231212 (claim fn) run on deploy. To grant Krystal: insert into finance.share_invites(owner_workspace_id, email) values (67, '<email>') for each candidate email; she auto-claims on signup/login with whichever she uses. Revoke: delete her finance.shares row (+ any leftover share_invites).
[Claude Code, 2026-07-07]
Status update 2026-07-14¶
Sharing v2 (feat/finance-share-settings: consent + viewer/co-owner roles) is DEPLOYED to prod. It had been merged to local main earlier on 2026-07-14 but never pushed or deployed; the links-form-blocks-ux ship (merge 2f55acb) built and pushed main as-is, carrying it live. Follow-up for Justin: re-send the share invite to Krystal.
[Claude Code]