Skip to main content
Docs

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

Important

See the demo repo for a full example of how to integrate Supabase with Clerk in a Next.js app.

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.

Tip

This integration restricts what data authenticated users can access in the database, but does not synchronize user records between Clerk and Supabase. To send additional data from Clerk to your Supabase database, use webhooks.

Setup Clerk as a Supabase third-party auth provider

First, we need to setup Clerk as a third-party auth provider in Supabase.

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.

terminal
npm i @supabase/supabase-js
terminal
yarn add @supabase/supabase-js
terminal
pnpm add @supabase/supabase-js
terminal
bun add @supabase/supabase-js

Set up your environment variables

  1. In the sidenav of the Supabase dashboard
  2. Add the Project URL to your .env file as SUPABASE_URL.
  3. In the Project API keys section, add the value beside anon public to your .env file as SUPABASE_KEY.

Important

If you are using Next.js, the NEXT_PUBLIC_ prefix is required for environment variables that are used in the client-side code.

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.

app/page.tsx
'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.

src/app/ssr/client.ts
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.

src/app/ssr/page.tsx
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>
  )
}
src/app/ssr/actions.ts
'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')
  }
}
src/app/ssr/AddTaskForm.tsx
'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

What did you think of this content?

Last updated on