Integrate Supabase with Clerk
You will learn the following:
- Use Clerk to authenticate access to your Supabase data
- Access Clerk user IDs in your Supabase RLS policies
Integrating Supabase with Clerk gives you the benefits of using a Supabase database while leveraging Clerk's authentication, prebuilt components, and webhooks. To get the most out of Supabase with Clerk, you must implement custom Row Level Security (RLS) policies.
RLS works by validating database queries according to the restrictions defined in the RLS policies applied to the table. This guide will show you how to create RLS policies that restrict access to data based on the user's Clerk ID. This way, users can only access data that belongs to them. To set this up, you will:
- Create a
user_id
column that defaults to the Clerk user's ID when new records are created. - Create policies to restrict what data can be read and inserted.
- Use the Clerk Supabase integration helper in your code to authenticate with Supabase and execute queries.
This guide will have you create a new table in your Supabase project, but you can apply these concepts to your existing tables as well.
Setup Clerk as a Supabase third-party auth provider
First, we need to setup Clerk as a third-party auth provider in Supabase.
- Navigate to the Supabase integration setup to enable the Supabase integration for your Clerk instance. Copy the Clerk domain for the next step.
- Navigate to the Third-party Auth settings in the Supabase dashboard, click Add provider, and select Clerk from the list of providers. Paste the domain you copied in the previous step into the field.
Your Clerk session token is now configured to work with Supabase.
Setup RLS policies using Clerk session token data
You can access Clerk session token data in Supabase using the built-in auth.jwt()
function. We can use this function to create custom RLS policies to restrict database access based on the requesting user.
First, let's create a table to enable RLS on. Open Supabase's SQL editor and run the following queries.
-- Create a "tasks" table with a user_id column that maps to a Clerk user ID
create table tasks(
id serial primary key,
name text not null,
user_id text not null default auth.jwt()->>'sub'
);
-- Enable RLS on the table
alter table "tasks" enable row level security;
Next, create two policies that restrict access to the tasks
table based on the requesting user's Clerk ID.
create policy "User can view their own tasks"
on "public"."tasks"
for select
to authenticated
using (
((select auth.jwt()->>'sub') = (user_id)::text)
);
create policy "Users must insert their own tasks"
on "public"."tasks"
as permissive
for insert
to authenticated
with check (
((select auth.jwt()->>'sub') = (user_id)::text)
);
Install the Supabase client library
Add the Supabase client library to your project.
npm i @supabase/supabase-js
yarn add @supabase/supabase-js
pnpm add @supabase/supabase-js
bun add @supabase/supabase-js
Set up your environment variables
- In the sidenav of the Supabase dashboard
- Add the Project URL to your
.env
file asSUPABASE_URL
. - In the Project API keys section, add the value beside
anon
public
to your.env
file asSUPABASE_KEY
.
Fetch Supabase data in your code
The following example shows the list of tasks for the user and allows the user to add new tasks.
The createClerkSupabaseClient()
function uses Supabase's createClient()
method to initialize a new Supabase client with access to Clerk's session token.
The following example uses the Next.js SDK to access the useUser()
and useSession()
hooks, but you can adapt this code to work with any React-based Clerk SDK.
'use client'
import { useEffect, useState } from 'react'
import { useSession, useUser } from '@clerk/nextjs'
import { createClient } from '@supabase/supabase-js'
export default function Home() {
const [tasks, setTasks] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [name, setName] = useState('')
// The `useUser()` hook will be used to ensure that Clerk has loaded data about the logged in user
const { user } = useUser()
// The `useSession()` hook will be used to get the Clerk session object
const { session } = useSession()
// Create a custom supabase client that injects the Clerk Supabase token into the request headers
function createClerkSupabaseClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_KEY!,
{
async accessToken() {
return session?.getToken() ?? null
},
},
)
}
// Create a `client` object for accessing Supabase data using the Clerk token
const client = createClerkSupabaseClient()
// This `useEffect` will wait for the User object to be loaded before requesting
// the tasks for the logged in user
useEffect(() => {
if (!user) return
async function loadTasks() {
setLoading(true)
const { data, error } = await client.from('tasks').select()
if (!error) setTasks(data)
setLoading(false)
}
loadTasks()
}, [user])
async function createTask(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
// Insert task into the "tasks" database
await client.from('tasks').insert({
name,
})
window.location.reload()
}
return (
<div>
<h1>Tasks</h1>
{loading && <p>Loading...</p>}
{!loading && tasks.length > 0 && tasks.map((task: any) => <p>{task.name}</p>)}
{!loading && tasks.length === 0 && <p>No tasks found</p>}
<form onSubmit={createTask}>
<input
autoFocus
type="text"
name="name"
placeholder="Enter new task"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<button type="submit">Add</button>
</form>
</div>
)
}
The following example uses the Next.js SDK to demonstrate how to integrate Supabase with Clerk in a server-side rendered application.
The createServerSupabaseClient()
function is stored in a separate file so that it can be re-used in multiple places, such as within page.tsx
or a Server Action file. This function uses the auth().getToken()
method to pass the Clerk session token to the Supabase client.
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_KEY!,
{
async accessToken() {
return (await auth()).getToken()
},
},
)
}
The following files render the /ssr
page and handle the "Add task" form submission. Use the following tabs to view the code for each page.
import { createServerSupabaseClient } from './client'
import AddTaskForm from './AddTaskForm'
export default async function Home() {
// Use the custom Supabase client you created
const client = createServerSupabaseClient()
// Query the 'tasks' table to render the list of tasks
const { data, error } = await client.from('tasks').select()
if (error) {
throw error
}
const tasks = data
return (
<div>
<h1>Tasks</h1>
<div>{tasks?.map((task: any) => <p key={task.id}>{task.name}</p>)}</div>
<AddTaskForm />
</div>
)
}
'use server'
import { createServerSupabaseClient } from './client'
const client = createServerSupabaseClient()
export async function addTask(name: string) {
try {
const response = await client.from('tasks').insert({
name,
})
console.log('Task successfully added!', response)
} catch (error: any) {
console.error('Error adding task:', error.message)
throw new Error('Failed to add task')
}
}
'use client'
import React, { useState } from 'react'
import { addTask } from './actions'
import { useRouter } from 'next/navigation'
function AddTaskForm() {
const [taskName, setTaskName] = useState('')
const router = useRouter()
async function onSubmit() {
await addTask(taskName)
setTaskName('')
router.refresh()
}
return (
<form action={onSubmit}>
<input
autoFocus
type="text"
name="name"
placeholder="Enter new task"
onChange={(e) => setTaskName(e.target.value)}
value={taskName}
/>
<button type="submit">Add</button>
</form>
)
}
export default AddTaskForm
Test your integration
Run your project and sign in. Test creating and viewing tasks. Sign out and sign in as a different user, and repeat.
If you have the same tasks across multiple accounts, double check that RLS is enabled, or that the RLS policies were properly created. Check the table in the Supabase dashboard. You should see all the tasks between both users, but with differing values in the user_id
column.
What does the Clerk Supabase integration do?
Requests to Supabase's APIs require that authenticated users have a "role": "authenticated"
JWT claim. When enabled, the Clerk Supabase integration adds this claim to your instance's generated session tokens.
Supabase JWT template deprecation
As of April 1st, 2025, the Clerk Supabase JWT template is considered deprecated. Going forward, the native Supabase integration is the recommended way to integrate Clerk with Supabase. The native integration has a number of benefits over the JWT template:
- No need to fetch a new token for each Supabase request
- No need to share your Supabase JWT secret key with Clerk
For more information on the benefits of the native integration, see Supabase's documentation on third-party auth providers.
Feedback
Last updated on