User owns data
-- Users can view/edit their own dataUSING (((select auth.jwt())->>'sub')::text = clerk_id)This guide shows you how to integrate Clerk authentication with Supabase database using the 2025 native third-party authentication method.
The native integration allows Supabase to directly accept Clerk-signed session tokens without requiring custom JWT templates or token generation.
my-app.clerk.accounts.dev)# Clerk ConfigurationPUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxCLERK_SECRET_KEY=sk_live_xxxxxCLERK_WEBHOOK_SECRET=whsec_xxxxx # Optional, for user sync
# Supabase ConfigurationSUPABASE_URL=https://your-project.supabase.coSUPABASE_ANON_KEY=eyJhbGci...SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # For webhooks only
# Public keys (client-side)PUBLIC_SUPABASE_URL=https://your-project.supabase.coPUBLIC_SUPABASE_ANON_KEY=eyJhbGci...Run these migrations in your Supabase SQL Editor:
-- Copy and paste from:-- scripts/migrations/001_core_schema.sql
-- Creates:-- - users table-- - organization_memberships table-- - user_preferences table-- - Indexes and triggers-- Copy and paste from:-- scripts/migrations/002_security_policies.sql
-- Creates RLS policies for:-- - User data access-- - Organization member access-- - Admin-level accessUse the Clerk token from middleware to create authenticated Supabase clients:
import type { APIRoute } from 'astro'import { createServerSupabaseClient } from '#libs/supabase-native'
export const GET: APIRoute = async ({ locals }) => { const { clerkToken, userId } = locals
const supabase = createServerSupabaseClient(clerkToken)
const { data, error } = await supabase .from('users') .select('*') .eq('clerk_id', userId) .single()
return new Response(JSON.stringify(data))}Use the useSupabase hook for automatic token handling:
import { useSupabase } from '#hooks/useSupabase'
export function UserProfile() { const { supabase, userId, isLoaded } = useSupabase() const [profile, setProfile] = useState(null)
useEffect(() => { if (!supabase || !userId) return
supabase .from('users') .select('*') .eq('clerk_id', userId) .single() .then(({ data }) => setProfile(data)) }, [supabase, userId])
if (!isLoaded) return <div>Loading...</div>
return <div>{profile?.full_name}</div>}Supabase RLS policies use Clerk’s JWT claims to control data access:
-- Users can only see their own profileCREATE POLICY "users_select_own" ON users FOR SELECT USING (((select auth.jwt())->>'sub')::text = clerk_id);How it works:
sub claim (user ID)auth.jwt()->>'sub'clerk_id columnUser owns data
-- Users can view/edit their own dataUSING (((select auth.jwt())->>'sub')::text = clerk_id)Organization admin access
-- Org admins see all members in their orgUSING ( clerk_org_id IN ( SELECT clerk_org_id FROM organization_memberships WHERE user_id IN ( SELECT id FROM users WHERE clerk_id = ((select auth.jwt())->>'sub')::text ) AND clerk_org_role = 'org:admin' ))Service role (webhooks)
-- Bypass RLS for admin operationsUSING ((select auth.role()) = 'service_role')Problem: Queries return empty despite data existing.
Solution:
role: "authenticated" claimDebug:
const token = await auth().getToken()console.log('JWT Claims:', JSON.parse(atob(token.split('.')[1])))Problem: Authenticated users can’t access their own data.
Solution:
auth.jwt()->>'sub' (not auth.uid())users tableclerk_id matches JWT sub claimTest in SQL Editor:
SELECT (select auth.jwt()->>'sub') as jwt_sub, clerk_id, clerk_id = (select auth.jwt()->>'sub')::text as matchesFROM users;Problem: Users don’t appear in Supabase after signup.
Solution:
.env valueLocal testing with ngrok:
ngrok http 4321# Update Clerk webhook to: https://abc123.ngrok.io/api/webhooks/clerkIf you’re using the deprecated JWT template method:
{ template: 'supabase' } parameterSUPABASE_JWT_SECRET from environment variablesLast Updated: 2025-10-06 | Method: Native Third-Party Auth