Signal/docs/phase3-setup-checklist.md
Kisa ec2cd24bd7 Add Phase 3: Clerk auth with org-scoped data isolation
Backend: JWT middleware validates Clerk tokens on every request,
extracts org ID from claims, enforces org-scoped queries via
Supabase RLS. Frontend: ClerkProvider wraps the app, auth gate
blocks unauthenticated access, UserButton in header, token
injected into every API call. Supabase production wired to trust
Clerk JWTs via Third-Party Auth integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 12:12:17 -04:00

5.7 KiB

Phase 3 Setup Checklist

Clerk + Supabase + Railway Staging

Run these steps in order. Code is already written — you just need the keys.


Step 1: Clerk Account Setup

  1. Go to clerk.com and create an account

  2. Create a new application — name it Signal

  3. Select React as the framework

  4. From Dashboard > API Keys, copy:

    • Publishable Key (starts with pk_test_...) — goes in the frontend .env
    • Secret Key (starts with sk_test_...) — NOT needed for our approach (we use JWKS)
  5. From Dashboard > API Keys > Show API URLs, copy the Frontend API URL

    • Looks like: https://clean-mayfly-62.clerk.accounts.dev
    • Your JWKS URL is: <Frontend API URL>/.well-known/jwks.json
    • Example: https://clean-mayfly-62.clerk.accounts.dev/.well-known/jwks.json
    • This goes in Railway as CLERK_JWKS_URL
  6. From Dashboard > Configure > Organizations, enable Organizations

    • Create one organization named Gaboro DME with slug gaboro-pilot
    • Copy the Organization ID (starts with org_...) — you'll need it for the SQL step below
    • Add yourself as a member of that organization
  7. From Dashboard > Configure > Domains, add:

    • http://localhost:5173 (local dev)
    • Your Vercel production URL when it's live

Step 2: Supabase Staging Project

  1. Go to supabase.com, create a new project named signal-staging
  2. Wait for it to provision (2-3 minutes)
  3. Go to Settings > API, copy:
    • Project URL (looks like https://xxxxxxxxxxxx.supabase.co)
    • Service Role Key — must be the long eyJ... JWT format (NOT the sb_ format)
  4. Go to SQL Editor, run the migration SQL below

Step 3: SQL Migration

Run on BOTH Supabase projects (production + staging)

-- Add Clerk org ID column to organizations table
ALTER TABLE organizations
  ADD COLUMN IF NOT EXISTS clerk_org_id TEXT UNIQUE;

-- Update the pilot org with the real Clerk org ID
-- Replace org_xxxxxxxxxxxxxxxx with the actual org ID from Clerk Dashboard
UPDATE organizations
  SET clerk_org_id = 'org_xxxxxxxxxxxxxxxx'
  WHERE slug = 'gaboro-pilot';

-- Helper function for RLS policies (reads Clerk org ID from JWT)
CREATE OR REPLACE FUNCTION requesting_owner_id()
RETURNS TEXT
LANGUAGE SQL
STABLE
AS $$
  SELECT COALESCE(
    (auth.jwt() -> 'o') ->> 'id',
    auth.jwt() ->> 'sub'
  );
$$;

-- RLS on organizations
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_members_see_own_org"
  ON organizations FOR SELECT
  USING (clerk_org_id = requesting_owner_id());

-- RLS on upload_batches
ALTER TABLE upload_batches ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_members_see_own_batches"
  ON upload_batches FOR SELECT
  USING (org_id IN (
    SELECT id FROM organizations WHERE clerk_org_id = requesting_owner_id()
  ));
CREATE POLICY "org_members_insert_own_batches"
  ON upload_batches FOR INSERT
  WITH CHECK (org_id IN (
    SELECT id FROM organizations WHERE clerk_org_id = requesting_owner_id()
  ));

-- RLS on normalized_records
ALTER TABLE normalized_records ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_members_see_own_records"
  ON normalized_records FOR SELECT
  USING (EXISTS (
    SELECT 1 FROM upload_batches ub
    JOIN organizations o ON o.id = ub.org_id
    WHERE ub.id = normalized_records.batch_id
      AND o.clerk_org_id = requesting_owner_id()
  ));

-- RLS on report_runs
ALTER TABLE report_runs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_members_see_own_runs"
  ON report_runs FOR SELECT
  USING (org_id IN (
    SELECT id FROM organizations WHERE clerk_org_id = requesting_owner_id()
  ));

-- RLS on export_files
ALTER TABLE export_files ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_members_see_own_exports"
  ON export_files FOR SELECT
  USING (EXISTS (
    SELECT 1 FROM report_runs rr
    JOIN organizations o ON o.id = rr.org_id
    WHERE rr.id = export_files.report_run_id
      AND o.clerk_org_id = requesting_owner_id()
  ));

-- NOTE: source_files and mapping_decisions are write-once audit records.
-- The backend uses the service role key for writes (bypasses RLS by design).
-- Add SELECT policies here when frontend reads are needed in a later phase.

Step 4: Connect Supabase to Clerk (Native Integration)

In Clerk Dashboard:

  1. Go to Configure > Integrations (or search "Supabase")
  2. Click Activate Supabase integration
  3. Copy your Clerk domain shown there

In Supabase Dashboard (repeat for BOTH projects):

  1. Go to Authentication > Sign In / Sign Up > Third Party Auth
  2. Click Add provider > select Clerk
  3. Paste the Clerk domain
  4. Save

This makes Supabase trust Clerk JWTs — no custom templates needed.


Step 5: Set Railway Environment Variables

Production environment:

In Railway > signal-api > production > Variables, add:

CLERK_JWKS_URL = https://eternal-goblin-1.clerk.accounts.dev/.well-known/jwks.json

Staging environment:

In Railway > signal-api > staging > Variables, add:

CLERK_JWKS_URL    = https://eternal-goblin-1.clerk.accounts.dev/.well-known/jwks.json
SUPABASE_URL      = https://<staging-project>.supabase.co
SUPABASE_ANON_KEY = eyJ... (anon key from staging project)
SUPABASE_SERVICE_KEY = eyJ... (service role key from staging project)

Staging already has SIGNAL_API_KEY and ALLOWED_ORIGINS set.


Step 6: Set Frontend .env

Edit signal-ui/.env:

VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxx

Step 7: Deploy Staging Backend

Once all Railway staging vars are set, trigger a deploy:

cd /Users/sttil-solutions/projects/signal
railway up --detach --environment staging -m "Phase 3: Clerk auth + staging env"

Staging API Key (save this)

The staging environment uses a separate API key for direct testing:

r0C6vm1RLeg84lB9jeZ5sSRieDZWsLL0dAMDfEcXubE