Files
multica/server/migrations/065_backfill_onboarded_at.up.sql
Naiyuan Qing 51bc5a818f fix(onboarding): decouple from workspace state and route invitees correctly (#1936)
PR #1868 conflated "has workspace" with "completed onboarding" —
restore `onboarded_at` as the single signal, and route invited users
through a dedicated /invitations page before they ever see onboarding.

- Backend: CreateWorkspace + AcceptInvitation atomically set
  onboarded_at alongside the member insert, establishing the
  invariant "member row exists ↔ onboarded_at != null" at the DB
  layer.
- Migration 065: one-shot backfill closes the dirty rows produced
  by PR #1868 (users with a workspace but onboarded_at == null).
- Entry points (web callback, login, desktop App): if onboarded_at
  is null, look up pending invitations by email and route to the
  new batch /invitations page; otherwise the resolver picks
  workspace / new-workspace as before.
- OnboardingPage: stops bouncing on hasWorkspaces; only
  hasOnboarded bounces. Unblocks the user from completing
  Step 3 (workspace creation) → Steps 4 / 5.
- StarterContentPrompt: only shows when the user is the solo
  member of the workspace, so invited users never get prompted to
  import starter content into someone else's workspace.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:05:53 +08:00

14 lines
747 B
SQL

-- Backfill onboarded_at for users who already belong to a workspace.
-- PR #1868 (since reverted) routed users by hasWorkspace instead of onboarded_at,
-- producing a population of users with workspace memberships but
-- onboarded_at == NULL. After the new design enforces
-- "member row exists ↔ onboarded_at != null" via backend transactions,
-- this one-shot backfill cleans existing dirty rows.
--
-- Uses created_at as the timestamp because these users were de facto onboarded
-- when their account was first created — backfilling with now() would distort
-- onboarding-funnel analytics. COALESCE keeps it idempotent.
UPDATE "user"
SET onboarded_at = COALESCE(onboarded_at, created_at)
WHERE id IN (SELECT DISTINCT user_id FROM member);