Skip to content

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)

  1. Per-user Google connection store (lib/google-connections.ts + /api/google/connect[?groups=calendar,gmail,youtube&redirect=...] + callback). Reuses app.api_connections (AES-256-GCM at rest) with provider google, label = account email, encrypted refresh token. HMAC-signed OAuth state (AUTH_SECRET), uid must match live session; per-connection access-token cache.
  2. Calendar per-user. Migration 20260706T180547: user_id on calendar_events/calendar_sync_state (Google rows backfilled to Justin), source keys widened to google:<connection id>. tokenForSource() in lib/google-calendar.ts is 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.
  3. Social workspace-level. Migration 20260706T181332: optional workspace_id on api_connections, membership RLS, partial unique keys per scope. lib/social/data.ts reads workspace connections first (personal fallback per platform); owner-token YouTube fallback now gated to Justin (was reachable by any caller once un-gated). social dropped from OWNER_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=gmail scope group is already defined).
  • OAuth callback untested end-to-end: needs https://<prod-host>/api/google/connect/callback added 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's google connection.
  • 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_events CHECK constraint widened; api_connections RLS policy replaced.

[Claude Code]