-- ── Migration 015: Email as universal user identity ─────────────────────────── -- -- Background -- ---------- -- Prior migrations stored Clerk opaque user IDs (e.g. `user_2abc…`) in every -- `user_id` / `owner_id` / `author_id` column. Those IDs are internal to Clerk -- and cannot be resolved back to humans without an extra Clerk API call. The -- only durable, human-readable identity available in this stack is the user's -- primary email address. -- -- What this migration does -- ------------------------ -- 1. Renames identity columns in all tables: -- team_members.user_id → email -- workspaces.owner_id → owner_email -- intent_diffs.author_id → author_email -- -- 2. Drops all stale RLS policies that referenced the old column names or -- used `auth.uid()::text` (which returns a UUID-shaped sub claim, not an -- email) and recreates every policy to compare against -- `auth.jwt() ->> 'email'` — the email claim injected by the Clerk -- Supabase JWT template. -- -- 3. Updates the `is_workspace_member` helper function from migration 003 -- to use the renamed column. -- -- Application changes (applied in tandem) -- ---------------------------------------- -- • API routes use currentUser().primaryEmailAddress.emailAddress instead of -- auth().userId for all DB identity lookups. -- • The invite UI accepts an email address instead of a Clerk user ID. -- -- Idempotency -- ----------- -- Column renames are guarded by DO $$ blocks that skip if the target column -- already exists. Policy drops use DROP … IF EXISTS. Safe to re-run. -- ══════════════════════════════════════════════════════════════════════════════ -- 1. Column renames -- ══════════════════════════════════════════════════════════════════════════════ -- ── team_members: user_id → email ──────────────────────────────────────────── DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'team_members' AND column_name = 'user_id' ) AND NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'team_members' AND column_name = 'email' ) THEN ALTER TABLE team_members RENAME COLUMN user_id TO email; END IF; END $$; -- Replace the unique constraint (workspace_id, user_id) with (workspace_id, email). ALTER TABLE team_members DROP CONSTRAINT IF EXISTS team_members_workspace_id_user_id_key; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'team_members_workspace_id_email_key' ) THEN ALTER TABLE team_members ADD CONSTRAINT team_members_workspace_id_email_key UNIQUE (workspace_id, email); END IF; END $$; -- Replace the index. DROP INDEX IF EXISTS team_members_user_id_idx; CREATE INDEX IF NOT EXISTS team_members_email_idx ON team_members (email); -- ── workspaces: owner_id → owner_email ─────────────────────────────────────── DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'workspaces' AND column_name = 'owner_id' ) AND NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'workspaces' AND column_name = 'owner_email' ) THEN ALTER TABLE workspaces RENAME COLUMN owner_id TO owner_email; END IF; END $$; DROP INDEX IF EXISTS workspaces_owner_id_idx; CREATE INDEX IF NOT EXISTS workspaces_owner_email_idx ON workspaces (owner_email); -- ── intent_diffs: author_id → author_email ─────────────────────────────────── DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'intent_diffs' AND column_name = 'author_id' ) AND NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'intent_diffs' AND column_name = 'author_email' ) THEN ALTER TABLE intent_diffs RENAME COLUMN author_id TO author_email; END IF; END $$; -- ══════════════════════════════════════════════════════════════════════════════ -- 2. Drop all stale RLS policies -- ══════════════════════════════════════════════════════════════════════════════ -- workspaces DROP POLICY IF EXISTS workspaces_select ON workspaces; DROP POLICY IF EXISTS workspaces_insert ON workspaces; DROP POLICY IF EXISTS workspaces_update ON workspaces; DROP POLICY IF EXISTS "workspace owner access" ON workspaces; -- artboards DROP POLICY IF EXISTS artboards_select ON artboards; DROP POLICY IF EXISTS artboards_insert ON artboards; DROP POLICY IF EXISTS artboards_update ON artboards; DROP POLICY IF EXISTS artboards_delete ON artboards; DROP POLICY IF EXISTS "workspace member artboard access" ON artboards; -- origins DROP POLICY IF EXISTS "workspace member origin access" ON origins; -- intent_diffs DROP POLICY IF EXISTS intent_diffs_select ON intent_diffs; DROP POLICY IF EXISTS intent_diffs_insert ON intent_diffs; DROP POLICY IF EXISTS intent_diffs_update ON intent_diffs; DROP POLICY IF EXISTS "workspace member diff access" ON intent_diffs; -- agent_sessions DROP POLICY IF EXISTS agent_sessions_select ON agent_sessions; DROP POLICY IF EXISTS agent_sessions_insert ON agent_sessions; DROP POLICY IF EXISTS agent_sessions_update ON agent_sessions; DROP POLICY IF EXISTS "workspace member session access" ON agent_sessions; -- design_language_files DROP POLICY IF EXISTS dlf_select ON design_language_files; DROP POLICY IF EXISTS dlf_insert ON design_language_files; DROP POLICY IF EXISTS dlf_update ON design_language_files; DROP POLICY IF EXISTS "workspace member dlf access" ON design_language_files; DROP POLICY IF EXISTS dlf_workspace_select ON design_language_files; DROP POLICY IF EXISTS dlf_workspace_write ON design_language_files; -- team_members DROP POLICY IF EXISTS team_members_select ON team_members; DROP POLICY IF EXISTS team_members_insert ON team_members; DROP POLICY IF EXISTS team_members_delete ON team_members; -- design_languages (migration 014) DROP POLICY IF EXISTS dl_workspace_member_select ON design_languages; DROP POLICY IF EXISTS dl_workspace_member_write ON design_languages; DROP POLICY IF EXISTS "design_languages_workspace_member_select" ON design_languages; DROP POLICY IF EXISTS "design_languages_workspace_member_insert" ON design_languages; DROP POLICY IF EXISTS "design_languages_workspace_member_update" ON design_languages; -- design_language_versions (migration 014) DROP POLICY IF EXISTS dlv_workspace_member_select ON design_language_versions; DROP POLICY IF EXISTS "design_language_versions_workspace_member_select" ON design_language_versions; DROP POLICY IF EXISTS "design_language_versions_workspace_member_insert" ON design_language_versions; -- ══════════════════════════════════════════════════════════════════════════════ -- 3. Update the is_workspace_member helper (migration 003) -- ══════════════════════════════════════════════════════════════════════════════ -- Recreate with team_members.email and auth.jwt() ->> 'email'. CREATE OR REPLACE FUNCTION is_workspace_member(ws_id UUID) RETURNS BOOLEAN LANGUAGE sql SECURITY DEFINER AS $$ SELECT EXISTS ( SELECT 1 FROM team_members WHERE workspace_id = ws_id AND email = auth.jwt() ->> 'email' ); $$; -- ══════════════════════════════════════════════════════════════════════════════ -- 4. Recreate all RLS policies with email-based identity -- ══════════════════════════════════════════════════════════════════════════════ -- -- Identity predicate throughout: auth.jwt() ->> 'email' -- This reads the `email` claim from the Clerk Supabase JWT template. -- -- For workspace owners the check is: workspaces.owner_email = auth.jwt() ->> 'email' -- For general membership: team_members.email = auth.jwt() ->> 'email' -- ── workspaces ──────────────────────────────────────────────────────────────── CREATE POLICY workspaces_select ON workspaces FOR SELECT USING (is_workspace_member(id)); CREATE POLICY workspaces_insert ON workspaces FOR INSERT WITH CHECK (owner_email = auth.jwt() ->> 'email'); CREATE POLICY workspaces_update ON workspaces FOR UPDATE USING (owner_email = auth.jwt() ->> 'email'); -- ── artboards ───────────────────────────────────────────────────────────────── CREATE POLICY artboards_select ON artboards FOR SELECT USING (is_workspace_member(workspace_id)); CREATE POLICY artboards_insert ON artboards FOR INSERT WITH CHECK (is_workspace_member(workspace_id)); CREATE POLICY artboards_update ON artboards FOR UPDATE USING (is_workspace_member(workspace_id)); CREATE POLICY artboards_delete ON artboards FOR DELETE USING (is_workspace_member(workspace_id)); -- ── origins ─────────────────────────────────────────────────────────────────── -- Accepts both FK directions (spec: origins.artboard_id; legacy: artboards.origin_id). CREATE POLICY "workspace member origin access" ON origins FOR ALL USING ( -- spec-compliant: origins.artboard_id set to the artboard's uuid artboard_id IN ( SELECT id FROM artboards WHERE is_workspace_member(workspace_id) ) OR -- legacy: artboards.origin_id points at this origin row id IN ( SELECT origin_id FROM artboards WHERE is_workspace_member(workspace_id) AND origin_id IS NOT NULL ) ); -- ── intent_diffs ────────────────────────────────────────────────────────────── CREATE POLICY intent_diffs_select ON intent_diffs FOR SELECT USING (is_workspace_member( (SELECT workspace_id FROM artboards WHERE id = artboard_id) )); CREATE POLICY intent_diffs_insert ON intent_diffs FOR INSERT WITH CHECK (is_workspace_member( (SELECT workspace_id FROM artboards WHERE id = artboard_id) )); CREATE POLICY intent_diffs_update ON intent_diffs FOR UPDATE USING (is_workspace_member( (SELECT workspace_id FROM artboards WHERE id = artboard_id) )); -- ── agent_sessions ──────────────────────────────────────────────────────────── CREATE POLICY agent_sessions_select ON agent_sessions FOR SELECT USING (is_workspace_member( (SELECT workspace_id FROM artboards WHERE id = artboard_id) )); CREATE POLICY agent_sessions_insert ON agent_sessions FOR INSERT WITH CHECK (is_workspace_member( (SELECT workspace_id FROM artboards WHERE id = artboard_id) )); CREATE POLICY agent_sessions_update ON agent_sessions FOR UPDATE USING (is_workspace_member( (SELECT workspace_id FROM artboards WHERE id = artboard_id) )); -- ── design_language_files ───────────────────────────────────────────────────── CREATE POLICY dlf_workspace_select ON design_language_files FOR SELECT USING (is_workspace_member(workspace_id)); CREATE POLICY dlf_workspace_write ON design_language_files FOR ALL USING ( workspace_id IN ( SELECT workspace_id FROM team_members WHERE email = auth.jwt() ->> 'email' AND role IN ('OWNER', 'DESIGNER') ) ); -- ── team_members ────────────────────────────────────────────────────────────── CREATE POLICY team_members_select ON team_members FOR SELECT USING (is_workspace_member(workspace_id)); -- Only workspace owners can add members. CREATE POLICY team_members_insert ON team_members FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 FROM workspaces WHERE id = workspace_id AND owner_email = auth.jwt() ->> 'email' ) ); -- Owners can remove anyone; members can remove themselves. CREATE POLICY team_members_delete ON team_members FOR DELETE USING ( EXISTS ( SELECT 1 FROM workspaces WHERE id = workspace_id AND owner_email = auth.jwt() ->> 'email' ) OR email = auth.jwt() ->> 'email' ); -- ── design_languages ────────────────────────────────────────────────────────── CREATE POLICY dl_workspace_member_select ON design_languages FOR SELECT USING (is_workspace_member(workspace_id)); CREATE POLICY dl_workspace_member_write ON design_languages FOR ALL USING (is_workspace_member(workspace_id)); -- ── design_language_versions ────────────────────────────────────────────────── CREATE POLICY dlv_workspace_member_select ON design_language_versions FOR SELECT USING ( design_language_id IN ( SELECT dl.id FROM design_languages dl WHERE is_workspace_member(dl.workspace_id) ) );