Seamplan
clerk → supabase

Why auth.uid() returns null in Supabase RLS when using Clerk

auth.uid() is null because Supabase is verifying a JWT that isn't signed with your Supabase JWT secret — usually a raw Clerk session token. Bridge Clerk → Supabase with a signed Supabase-compatible JWT (or the official third-party auth integration), then RLS sees a real uid.

Why it happens

Supabase RLS helpers like auth.uid() read claims from the JWT attached to the request. Clerk's session token is a valid JWT for Clerk — not for Supabase. If you pass getToken() straight into createClient({ global: { headers: { Authorization }}}) without minting a Supabase-shaped token (or enabling Clerk as a third-party auth provider), PostgREST accepts the connection as anon and auth.uid() resolves to null. The seam is the audience/issuer mismatch, not your RLS SQL.

Env-var matrix

Variable · which dashboard · publishable / secret / signing · differs per environment?

env-var matrix
variabledashboardkindper env?
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYClerkpublishableyes
CLERK_SECRET_KEYClerksecretyes
NEXT_PUBLIC_SUPABASE_URLSupabasepublishableyes
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabasepublishableyes
SUPABASE_JWT_SECRET
used to mint a Supabase-compatible JWT from the Clerk user id
Supabase → Settings → APIsigningno

The fix

ts
import { auth } from "@clerk/nextjs/server";
import { createClient } from "@supabase/supabase-js";
import { SignJWT } from "jose";

export async function createClerkSupabaseClient() {
  const { userId } = await auth();
  if (!userId) throw new Error("not signed in");

  const secret = new TextEncoder().encode(process.env.SUPABASE_JWT_SECRET!);
  const token = await new SignJWT({ role: "authenticated" })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuedAt()
    .setExpirationTime("1h")
    .sign(secret);

  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    { global: { headers: { Authorization: `Bearer ${token}` } } }
  );
}

What else breaks nearby

Companion repo

open the repo →

← all seams · plan this live