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>
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
-
Go to clerk.com and create an account
-
Create a new application — name it Signal
-
Select React as the framework
-
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)
- Publishable Key (starts with
-
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
- Looks like:
-
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
- Create one organization named Gaboro DME with slug
-
From Dashboard > Configure > Domains, add:
http://localhost:5173(local dev)- Your Vercel production URL when it's live
Step 2: Supabase Staging Project
- Go to supabase.com, create a new project named signal-staging
- Wait for it to provision (2-3 minutes)
- Go to Settings > API, copy:
- Project URL (looks like
https://xxxxxxxxxxxx.supabase.co) - Service Role Key — must be the long
eyJ...JWT format (NOT thesb_format)
- Project URL (looks like
- 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:
- Go to Configure > Integrations (or search "Supabase")
- Click Activate Supabase integration
- Copy your Clerk domain shown there
In Supabase Dashboard (repeat for BOTH projects):
- Go to Authentication > Sign In / Sign Up > Third Party Auth
- Click Add provider > select Clerk
- Paste the Clerk domain
- 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