Handoff: multiuser apps build (Calendar + Social) on feat/multiuser-apps-michael¶
URL: https://mkdocs.justinsforge.com/memory/handoffs/multiuser-apps-michael-build-2026-07-06/
Date: 2026-07-06
Branch: feat/multiuser-apps-michael (worktree ~/forge-suite-wt/feat/multiuser-apps-michael, dev slot :3061, DB ct_feat_multiuser_apps_michael). NOT merged, NOT deployed.
Implements: creatortrack-multiuser-apps-for-michael-2026-07-06 steps 1-3 (Email deferred pending Justin's scope answer).
What shipped (4 commits, 2eda344..63f6c9f)¶
- Per-user Google connection store (
lib/google-connections.ts+/api/google/connect[?groups=calendar,gmail,youtube&redirect=...]+ callback). Reusesapp.api_connections(AES-256-GCM at rest) with providergoogle, label = account email, encrypted refresh token. HMAC-signed OAuth state (AUTH_SECRET), uid must match live session; per-connection access-token cache. - Calendar per-user. Migration
20260706T180547:user_idoncalendar_events/calendar_sync_state(Google rows backfilled to Justin), source keys widened togoogle:<connection id>.tokenForSource()inlib/google-calendar.tsis the single chokepoint: forge pair = personal-apps owner only, connection keys must be caller-owned. Reads return local rows + caller's own Google rows only. Route un-gated for members; members get a "Connect Google Calendar" banner. - Social workspace-level. Migration
20260706T181332: optionalworkspace_idonapi_connections, membership RLS, partial unique keys per scope.lib/social/data.tsreads workspace connections first (personal fallback per platform); owner-token YouTube fallback now gated to Justin (was reachable by any caller once un-gated).socialdropped fromOWNER_ONLY_APP_IDS. Settings gained a scope selector (Only me / This workspace) + "shared with workspace" badge. Also fixed a latent bug: social sync read tokens from the raw encrypted envelope (always empty); now decrypted via the lib.
Verified¶
npx tsx --tsconfig scripts/tsconfig.smoke.json scripts/smoke-multiuser-apps.ts: 9/9 acceptance checks (Michael-analog cannot see/edit Justin's calendar rows, cannot mint forge or foreign-connection tokens, sees workspace social connections but not personal ones or non-member workspaces; Justin's owner paths unchanged).- tsc clean; eslint delta vs main = 0 (12 pre-existing errors untouched).
- Dev slot :3061: calendar page 200 (owner path), /social 200, calendar API 200, /api/google/connect 307 to a well-formed Google consent URL.
NOT done / for review¶
- Email stays owner-only. Per the plan: confirm with Justin whether Michael needs email inside CreatorTrack before building the per-user Gmail seam (the
groups=gmailscope group is already defined). - OAuth callback untested end-to-end: needs
https://<prod-host>/api/google/connect/callbackadded as an authorized redirect URI on the CreatorTrack Google OAuth client (plus calendar scope enabled on the consent screen). Dev-LAN http URIs can't be whitelisted, so first live test happens on prod after merge. - Workspace YouTube rows created via the old owner quick-connect (meta
auth:"forge_google_user") carry no token; a member syncing such a workspace gets a connect-state, not data. Follow-up: let a workspace youtube connection reference a member'sgoogleconnection. - Michael's real account + Gus workspace memberships: create at merge time via signup +
core.add_workspace_member(the smoke script shows the exact calls). - Merge notes: run both migrations on prod (idempotent);
calendar_eventsCHECK constraint widened; api_connections RLS policy replaced.
[Claude Code]