Clerk and Supabase are the most popular auth + database combination in the Next.js ecosystem. Connecting them correctly — so that Supabase's Row-Level Security can verify the user's Clerk identity — is now done with Clerk's native third-party auth support, with no JWT template and no shared secret. This guide covers the complete, current integration.
The Problem: Two Different Auth Systems
Clerk manages authentication and issues its own session tokens (JWTs). Supabase has its own auth system and uses JWTs for RLS. To use Clerk for auth and Supabase for data storage with RLS, you need Supabase to trust Clerk's session tokens. Clerk's native third-party auth support does exactly this — Supabase validates the token against your Clerk domain directly.
Step 1: Connect Clerk and Supabase with Third-Party Auth
- In the Clerk Dashboard, open the Supabase integration setup page and click Activate Supabase integration. Clerk reveals your Clerk domain (the token issuer) and automatically adds the "role": "authenticated" claim to session tokens.
- In the Supabase Dashboard, go to Authentication > Sign In / Providers > Third-Party Auth.
- Click Add provider and choose Clerk.
- Paste your Clerk domain (from the Clerk integration page) as the provider value and save.
- Supabase now trusts session tokens issued by Clerk — no JWT template and no shared JWT secret to copy.
Supabase validates Clerk's session tokens directly against your Clerk domain's public keys (JWKS). Because the native integration already adds the "role": "authenticated" claim, RLS runs against the tokens with no extra configuration.
Step 2: Create a Supabase Client That Passes the Clerk Session Token
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import { useSession } from '@clerk/nextjs';
// Client-side hook
export function useSupabaseClient() {
const { session } = useSession();
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
// Clerk session token is passed as the Supabase access token
accessToken: async () => session?.getToken() ?? null,
}
);
}// Server-side (Route Handlers and Server Actions)
import { auth } from '@clerk/nextjs/server';
import { createClient } from '@supabase/supabase-js';
export async function createServerSupabaseClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
accessToken: async () => (await auth()).getToken() ?? null,
}
);
}Step 3: Write RLS Policies Using Clerk's User ID
The Clerk JWT includes a sub claim containing the Clerk user ID. In Supabase RLS policies, auth.jwt()->>'sub' gives you this value.
-- Use auth.jwt()->>'sub' for the Clerk user ID
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see their own documents"
ON documents FOR SELECT
USING (auth.jwt()->>'sub' = clerk_user_id);
CREATE POLICY "Users can insert their own documents"
ON documents FOR INSERT
WITH CHECK (auth.jwt()->>'sub' = clerk_user_id);Store the Clerk user ID in a column named clerk_user_id (or user_id) in your tables. Reference it in RLS policies using auth.jwt()->>'sub'. Do not try to join against Supabase's auth.users table — Clerk manages users, not Supabase Auth.Step 4: Storing Clerk's org_id for Multi-Tenant RLS
If you're using Clerk Organizations, add an org_id claim to your Clerk session token (Clerk Dashboard > Sessions > customize the session token) so you can scope data to organisations.
// Session token claims (Clerk Dashboard > Sessions)
// The native integration adds "role": "authenticated" automatically.
// Add org_id yourself to scope data by organization:
{
"org_id": "{{org.id}}",
"org_role": "{{org.role}}"
}-- Org-scoped RLS policy using JWT claim
CREATE POLICY "Org members see org documents"
ON documents FOR SELECT
USING (org_id = (auth.jwt()->>'org_id'));Using the Integration in a Route Handler
// app/api/documents/route.ts
import { createServerSupabaseClient } from '@/lib/supabase';
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { userId } = await auth();
if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createServerSupabaseClient();
// RLS automatically filters to this user's documents
const { data, error } = await supabase.from('documents').select('*');
if (error) return Response.json({ error: error.message }, { status: 500 });
return Response.json(data);
}| Metadata | Value |
|---|---|
| Title | Clerk + Supabase: Native Third-Party Auth and Row-Level Security |
| Tool | Clerk |
| Primary SEO keyword | clerk supabase integration |
| Secondary keywords | clerk supabase third-party auth, clerk supabase RLS, clerk supabase next.js |
| Estimated read time | 8 minutes |
| Research date | 2026-07-23 |