u/pungaaisme

Using Clerk with Supabase? Don’t make the rookie RLS mistakes I made
▲ 2 r/Supabase+1 crossposts

Using Clerk with Supabase? Don’t make the rookie RLS mistakes I made

If you’re using Clerk with Supabase, you may have asked:

  • Why does auth.uid() return NULL when I send a Clerk token?
  • Should I use the service-role key to make RLS errors disappear?
  • How should Clerk Organizations map to tenants in Postgres?
  • How to handle a Clerk profile without an Organization?
  • How do I test RLS policies directly in SQL?
  • Why can RLS make a query over a tiny table take several seconds?
  • Can I use Supabase RLS without using Supabase Auth?

I kept finding variations of these questions while working with Clerk and Supabase.

The confusing part is that many answers mix three different integration paths:

  • Supabase Auth examples built around auth.uid()
  • The older Clerk JWT-template integration
  • Clerk and Supabase’s current native third-party authentication

For the current native integration, passing the token is surprisingly simple:

const supabase = createClient(url, publishableKey, {
  accessToken: async () => session?.getToken() ?? null,
})

But building a secure multi-tenant authorization model around that token requires more care.

Some mistakes are particularly easy to make:

  1. Using auth.uid() even though Clerk user IDs are strings such as user_....
  2. Trusting an organization ID supplied by the browser.
  3. Reaching for the service-role key, which bypasses RLS instead of fixing it.
  4. Querying a membership table recursively from its own RLS policies.
  5. Checking reads but forgetting WITH CHECK protections for inserts and updates.
  6. Running JWT and membership functions once per row instead of hoisting them.
  7. Testing only successful requests and never proving that another tenant is rejected.

With Clerk, the verified user and active organization context are available through the JWT:

(select auth.jwt()->>'sub')
(select auth.jwt()->'o'->>'id')
(select auth.jwt()->'o'->>'rol')

Postgres should derive the user and tenant from these verified claims.

The browser can send a Clerk session token and filter a query for performance, but it should never be the authority that decides which tenant owns a row.

I put together a step-by-step tutorial for anyone starting with Clerk and wanting to build a multi-tenant application using Supabase RLS:

Clerk and Supabase RLS Tenant Isolation

There is also a complete runnable repository:

Clerk + Supabase RLS demo

It includes:

  • Native Clerk session tokens without JWT templates
  • Personal and organization tenants
  • Clerk organization-role handling
  • Non-recursive authorization closures
  • Explicit SELECTINSERTUPDATE, and DELETE policies
  • Database-derived audit fields
  • Cross-tenant foreign-key protection
  • SQL contract and behavioral tests
  • A manual browser security matrix
  • Request-flow and data-model diagrams

My goal was to provide more than another isolated RLS snippet. The tutorial connects token delivery, claim extraction, tenant modeling, policy design, performance, and adversarial testing in one working example.

HTH fellow users just starting out on their Supabase & Clerk Journey

u/pungaaisme — 1 day ago