Integrate Supabase with Clerk
You will learn how to:
- Use Clerk to authenticate access to your Supabase data
- Access Clerk user IDs in your Supabase RLS policies
- Customize a JWT template to suit your use-case with Supabase
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(opens in a new tab) (RLS) policies.
Tutorial
This tutorial will teach you how to integrate Supabase with Clerk by creating RLS policies that show users content associated with their account.
You must have a local project with Clerk set up already to follow this tutorial. See the quickstart docs to get started.
Add a column for user IDs to your Supabase tables
To show users content scoped to their account, you must create RLS policies that check the user's Clerk ID. This requires storing Clerk user IDs on the relevant tables. In this guide we will use a column named user_id
, but you can use any name you would like.
- In the sidebar of your Supabase dashboard, navigate to Database > Tables. From here, you can add the user ID column to the table you want to use.
- Name the column
user_id
. - This column's data type must be
text
. - In the Default Value field, add
(requesting_user_id())
. This will make it default to the return value of the custom function you'll define in the next step. Doing this enables you to make each requesting user's ID available to Supabase from the request headers.
- Name the column
This step is required because Supabase's auth.uuid()
function, which normally grants access to the user ID in RLS policies, is not compatible with Clerk's user IDs.
Create a SQL query that checks the user ID
Create a requesting_user_id()
function, which will get the Clerk user ID of the requesting user from the request headers. This will allow you to access the user ID in your RLS policies.
- In the sidebar of your Supabase dashboard, navigate to SQL Editor, then select New query. Paste the following into the editor:
CREATE OR REPLACE FUNCTION requesting_user_id() RETURNS TEXT AS $$ SELECT NULLIF( current_setting('request.jwt.claims', true)::json->>'sub', '' )::text; $$ LANGUAGE SQL STABLE;
- Select Run to execute the query and create the
requesting_user_id
function.
Create ID-based RLS policies
Create RLS policies that allow users to modify and read content associated with their user IDs. This example will use an Addresses
table, but you can replace Addresses
with whatever table you're using.
- Create an RLS policy for inserting content:
- In your Supabase dashboard, in the sidebar, navigate to Authentication > Policies. Under the name of the table you want users to have access to, select New Policy.
- If you're using the policy editor, paste the following snippet, replacing
address
and"Addresses"
with whatever you want:Supabase policy editorCREATE POLICY "create user address" ON "public"."Addresses" AS PERMISSIVE FOR INSERT TO authenticated WITH CHECK (requesting_user_id() = user_id)
- If you're using the policy creator instead of the editor:
- Name the policy whatever you want.
- For Allowed operation, select INSERT.
- For Target roles, select authenticated.
- For the USING expression, paste the following:
Supabase policy editorrequesting_user_id() = user_id
- Create another RLS policy to allow users to read content from the same table they can modify. Follow the same instructions as the previous step, but the Allowed operation must be SELECT instead of INSERT.
- If you're using the editor, copy the same snippet from the previous step, replacing
FOR INSERT
withFOR SELECT
.
- If you're using the editor, copy the same snippet from the previous step, replacing
Get your Supabase JWT secret key
To give users access to your data, Supabase's API requires an authentication token. Your Clerk project can generate these authentication tokens, but it needs your Supabase project's JWT secret key first.
To find the JWT secret key:
- In the Supabase dashboard, select your project.
- In the sidebar, select Settings > API. Copy the value in the JWT Secret field.
- Open the Clerk dashboard(opens in a new tab) in a new tab.
Create a Supabase JWT template
Clerk's JWT templates allow you to generate a new valid Supabase authentication token for each signed in user. These tokens allow authenticated users to access your data with Supabase's API.
To create a JWT template for Supabase:
- Open your project in the Clerk Dashboard and navigate to the JWT Templates page in the sidebar.
- Select the New template button, then select Supabase from the list of options.
- Configure your template:
- The value of the Name field will be required when using the template in your code. For this tutorial, name it
supabase
. - Signing algorithm will be
HS256
by default. This algorithm is required to use JWTs with Supabase. Learn more in their docs(opens in a new tab). - Under Signing key, add the value of your Supabase JWT secret key from the previous step.
- Leave all other fields at their default settings unless you want to customize them. See Clerk's JWT template docs to learn what each of them do.
- Select Apply changes to complete setup.
- The value of the Name field will be required when using the template in your code. For this tutorial, name it
Set up your local project
To use Clerk with Supabase in your code, first install the necessary SDKs by running the following terminal command in the root directory of your project:
terminalnpm install @clerk/nextjs @supabase/supabase-js
terminalyarn add @clerk/nextjs @supabase/supabase-js
terminalpnpm add @clerk/nextjs @supabase/supabase-js
terminalnpm install @clerk/clerk-react @supabase/supabase-js
terminalyarn add @clerk/clerk-react @supabase/supabase-js
terminalpnpm add @clerk/clerk-react @supabase/supabase-js
Then, set up your environment variables:
- If you don't have a
.env.local
file in the root directory of your Next.js project, create one now. - Find your Clerk publishable key and secret key. If you're signed into Clerk, the
.env.local
snippet below will contain your keys. Otherwise:- Navigate to your Clerk Dashboard.
- Select your application, then select API Keys in the sidebar menu.
- You can copy your keys from the Quick Copy section.
- Add your keys to your
.env.local
file. - Find your Supabase credentials:
- Go to your Supabase dashboard. In the sidebar, select Settings > API.
- Copy the Project URL and add it to your
.env.local
file. - Copy the value beside
anon
public
in the Project API Keys section and add it to your.env.local
file.
The final result should be similar to this:
.env.localNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY={{pub_key}} NEXT_PUBLIC_SUPABASE_URL=your_supabase_url NEXT_PUBLIC_SUPABASE_KEY=your_supabase_anon_key
.env.localREACT_APP_CLERK_PUBLISHABLE_KEY={{pub_key}} REACT_APP_SUPABASE_URL=your_supabase_url REACT_APP_SUPABASE_KEY=your_supabase_anon_key
Fetch Supabase data in your code
The following steps will show you how to access content from your Supabase tables based on the user's ID. It assumes you have a table named "Addresses"
with a content
field, but you can adapt this code for any use case.
-
Create a component and define a
createClerkSupabaseClient
method. This method returns a client that connects to Supabase with an authentication token from your Clerk JWT template:app/supabase/page.tsx"use client"; import { createClient } from "@supabase/supabase-js"; import { useRef, useState } from "react"; // Add clerk to Window to avoid type errors declare global { interface Window { Clerk: any; } } function createClerkSupabaseClient() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, { global: { // Get the Supabase token with a custom fetch method fetch: async (url, options = {}) => { const clerkToken = await window.Clerk.session?.getToken({ template: "supabase", }); // Construct fetch headers const headers = new Headers(options?.headers); headers.set("Authorization", `Bearer ${clerkToken}`); // Now call the default fetch return fetch(url, { ...options, headers, }); }, }, } ); } const client = createClerkSupabaseClient();
pages/supabase/index.tsximport { createClient } from "@supabase/supabase-js"; import { useRef, useState } from "react"; // Add clerk to Window to avoid type errors declare global { interface Window { Clerk: any; } } function createClerkSupabaseClient() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, { global: { // Get the Supabase token with a custom fetch method fetch: async (url, options = {}) => { const clerkToken = await window.Clerk.session?.getToken({ template: "supabase", }); // Construct fetch headers const headers = new Headers(options?.headers); headers.set("Authorization", `Bearer ${clerkToken}`); // Now call the default fetch return fetch(url, { ...options, headers, }); }, }, } ); } const client = createClerkSupabaseClient();
components/supabase.tsximport { createClient } from "@supabase/supabase-js"; import React, { useRef, useState } from "react"; // Add clerk to Window to avoid type errors declare global { interface Window { Clerk: any; } } function createClerkSupabaseClient() { return createClient( process.env.REACT_APP_SUPABASE_URL!, process.env.REACT_APP_SUPABASE_KEY!, { global: { // Get the Supabase token with a custom fetch method fetch: async (url, options = {}) => { const clerkToken = await window.Clerk.session?.getToken({ template: "supabase", }); // Construct fetch headers const headers = new Headers(options?.headers); headers.set("Authorization", `Bearer ${clerkToken}`); // Now call the default fetch return fetch(url, { ...options, headers, }); }, }, } ); } const client = createClerkSupabaseClient();
-
Next, define a component with methods for listing addresses from and sending addresses to your database:
app/supabase/page.tsxexport default function Supabase() { const [addresses, setAddresses] = useState<any>(); const listAddresses = async () => { // Fetches all addresses scoped to the user // Replace "Addresses" with your table name const { data, error } = await client.from("Addresses").select(); if (!error) setAddresses(data); }; const inputRef = useRef<HTMLInputElement>(null); const sendAddress = async () => { if (!inputRef.current?.value) return; await client.from("Addresses").insert({ // Replace content with whatever field you want content: inputRef.current?.value, }); }; return null; }
pages/supabase/index.tsxexport default function Supabase() { const client = createClerkSupabaseClient(); const [addresses, setAddresses] = useState<any>(); const listAddresses = async () => { // Fetches all addresses scoped to the user // Replace "Addresses" with your table name const { data, error } = await client.from("Addresses").select(); if (!error) setAddresses(data); }; const inputRef = useRef<HTMLInputElement>(null); const sendAddress = async () => { if (!inputRef.current?.value) return; await client.from("Addresses").insert({ // Replace content with whatever field you want content: inputRef.current?.value, }); }; return null; }
component/supabase.tsxexport default function Supabase() { const client = createClerkSupabaseClient(); const [addresses, setAddresses] = useState<any>(); const listAddresses = async () => { // Fetches all addresses scoped to the user // Replace "Addresses" with your table name const { data, error } = await client.from("Addresses").select(); if (!error) setAddresses(data); }; const inputRef = useRef<HTMLInputElement>(null); const sendAddress = async () => { if (!inputRef.current?.value) return; await client.from("Addresses").insert({ // Replace content with whatever field you want content: inputRef.current?.value, }); }; return null; }
-
Finally, edit your component to return a basic UI that allows you to list all your addresses and send new ones:
app/supabase/page.tsxreturn ( <> <div style={{ display: "flex", flexDirection: "column" }}> <input onSubmit={sendAddress} type="text" ref={inputRef} /> <button onClick={sendAddress}>Send Address</button> <button onClick={listAddresses}>Fetch Addresses</button> </div> <h2>Addresses</h2> {!addresses ? ( <p>No addresses</p> ) : ( <ul> {addresses.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </> );
page/supabase/index.tsxreturn ( <> <div style={{ display: "flex", flexDirection: "column" }}> <input onSubmit={sendAddress} type="text" ref={inputRef} /> <button onClick={sendAddress}>Send Address</button> <button onClick={listAddresses}>Fetch Addresses</button> </div> <h2>Addresses</h2> {!addresses ? ( <p>No addresses</p> ) : ( <ul> {addresses.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </> );
components/supabase.tsxreturn ( <> <div style={{ display: "flex", flexDirection: "column" }}> <input onSubmit={sendAddress} type="text" ref={inputRef} /> <button onClick={sendAddress}>Send Address</button> <button onClick={listAddresses}>Fetch Addresses</button> </div> <h2>Addresses</h2> {!addresses ? ( <p>No addresses</p> ) : ( <ul> {addresses.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </> );
-
The final result should be similar to this:
app/supabase/page.tsx"use client"; import { createClient } from "@supabase/supabase-js"; import { useRef, useState } from "react"; // Add clerk to Window to avoid type errors declare global { interface Window { Clerk: any; } } function createClerkSupabaseClient() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, { global: { // Get the Supabase token with a custom fetch method fetch: async (url, options = {}) => { const clerkToken = await window.Clerk.session?.getToken({ template: "supabase", }); // Construct fetch headers const headers = new Headers(options?.headers); headers.set("Authorization", `Bearer ${clerkToken}`); // Now call the default fetch return fetch(url, { ...options, headers, }); }, }, } ); } const client = createClerkSupabaseClient(); export default function Supabase() { const [addresses, setAddresses] = useState<any>(); const listAddresses = async () => { // Fetches all addresses scoped to the user // Replace "Addresses" with your table name const { data, error } = await client.from("Addresses").select(); if (!error) setAddresses(data); }; const inputRef = useRef<HTMLInputElement>(null); const sendAddress = async () => { if (!inputRef.current?.value) return; await client.from("Addresses").insert({ // Replace content with whatever field you want content: inputRef.current?.value, }); }; return ( <> <div style={{ display: "flex", flexDirection: "column" }}> <input onSubmit={sendAddress} style={{ color: "black" }} type="text" ref={inputRef} /> <button onClick={sendAddress}>Send Address</button> <button onClick={listAddresses}>Fetch Addresses</button> </div> <h2>Addresses</h2> {!addresses ? ( <p>No addresses</p> ) : ( <ul> {addresses.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </> ); }
pages/supabase/index.tsximport { createClient } from "@supabase/supabase-js"; import { useRef, useState } from "react"; // Add clerk to Window to avoid type errors declare global { interface Window { Clerk: any; } } function createClerkSupabaseClient() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, { global: { // Get the Supabase token with a custom fetch method fetch: async (url, options = {}) => { const clerkToken = await window.Clerk.session?.getToken({ template: "supabase", }); // Construct fetch headers const headers = new Headers(options?.headers); headers.set("Authorization", `Bearer ${clerkToken}`); // Now call the default fetch return fetch(url, { ...options, headers, }); }, }, } ); } const client = createClerkSupabaseClient(); export default function Supabase() { const [addresses, setAddresses] = useState<any>(); const listAddresses = async () => { // Fetches all addresses scoped to the user // Replace "Addresses" with your table name const { data, error } = await client.from("Addresses").select(); if (!error) setAddresses(data); }; const inputRef = useRef<HTMLInputElement>(null); const sendAddress = async () => { if (!inputRef.current?.value) return; await client.from("Addresses").insert({ // Replace content with whatever field you want content: inputRef.current?.value, }); }; return ( <> <div style={{ display: "flex", flexDirection: "column" }}> <input onSubmit={sendAddress} style={{ color: "black" }} type="text" ref={inputRef} /> <button onClick={sendAddress}>Send Address</button> <button onClick={listAddresses}>Fetch Addresses</button> </div> <h2>Addresses</h2> {!addresses ? ( <p>No addresses</p> ) : ( <ul> {addresses.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </> ); }
components/supabase.tsximport { createClient } from "@supabase/supabase-js"; import React, { useRef, useState } from "react"; // Add clerk to Window to avoid type errors declare global { interface Window { Clerk: any; } } function createClerkSupabaseClient() { return createClient( process.env.REACT_APP_SUPABASE_URL!, process.env.REACT_APP_SUPABASE_KEY!, { global: { // Get the Supabase token with a custom fetch method fetch: async (url, options = {}) => { const clerkToken = await window.Clerk.session?.getToken({ template: "supabase", }); // Construct fetch headers const headers = new Headers(options?.headers); headers.set("Authorization", `Bearer ${clerkToken}`); // Now call the default fetch return fetch(url, { ...options, headers, }); }, }, } ); } const client = createClerkSupabaseClient(); export default function Supabase() { const [addresses, setAddresses] = useState<any>(); const listAddresses = async () => { // Fetches all addresses scoped to the user // Replace "Addresses" with your table name const { data, error } = await client.from("Addresses").select(); if (!error) setAddresses(data); }; const inputRef = useRef<HTMLInputElement>(null); const sendAddress = async () => { if (!inputRef.current?.value) return; await client.from("Addresses").insert({ // Replace content with whatever field you want content: inputRef.current?.value, }); }; return ( <> <div style={{ display: "flex", flexDirection: "column" }}> <input onSubmit={sendAddress} style={{ color: "black" }} type="text" ref={inputRef} /> <button onClick={sendAddress}>Send Address</button> <button onClick={listAddresses}>Fetch Addresses</button> </div> <h2>Addresses</h2> {!addresses ? ( <p>No addresses</p> ) : ( <ul> {addresses.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </> ); }
-
Try out your application. When you visit the page with your component, you'll be required to sign in. Try creating and fetching content.
-
To create a Supabase client in a Server component, you must first install the Supabase SSR package:
terminalnpm install @supabase/ssr
terminalyarn add @supabase/ssr
terminalpnpm add @supabase/ssr
-
Create a component and define a
createClerkSupabaseClient
method. This method returns a client that connects to Supabase with an authentication token from your Clerk JWT template:app/supabase/page.tsximport { auth } from "@clerk/nextjs/server"; import { CookieOptions, createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; async function createClerkSupabaseClient() { const cookieStore = cookies(); const { getToken } = auth(); const token = await getToken({ template: "supabase" }); const authToken = token ? { Authorization: `Bearer ${token}` } : null; return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, { global: { headers: { "Cache-Control": "no-store", ...authToken } }, cookies: { get(name: string) { return cookieStore.get(name)?.value; }, set(name: string, value: string, options: CookieOptions) { try { cookieStore.set({ name, value, ...options }); } catch (error) { // Handle the error } }, remove(name: string, options: CookieOptions) { try { cookieStore.set({ name, value: "", ...options }); } catch (error) { // Handle the error } }, }, } ); }
-
Next, define a component with methods for accessing a user's addresses from your database:
app/supabase/page.tsxexport default async function Supabase() { const client = await createClerkSupabaseClient(); const { data, error } = await client.from("Addresses").select(); if (error) { return <p>Error: {JSON.stringify(error, null, 2)}</p>; } return null; }
-
Finally, edit your component to return a basic UI that allows you to list all your addresses:
app/supabase/page.tsxreturn ( <div> <h2>Addresses</h2> {!data ? ( <p>No addresses</p> ) : ( <ul> {data.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </div> ); }
-
The final result should be similar to this:
app/supabase/page.tsximport { auth } from "@clerk/nextjs/server"; import { CookieOptions, createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; async function createClerkSupabaseClient() { const cookieStore = cookies(); const { getToken } = auth(); const token = await getToken({ template: "supabase" }); const authToken = token ? { Authorization: `Bearer ${token}` } : null; return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_KEY!, { global: { headers: { "Cache-Control": "no-store", ...authToken } }, cookies: { get(name: string) { return cookieStore.get(name)?.value; }, set(name: string, value: string, options: CookieOptions) { try { cookieStore.set({ name, value, ...options }); } catch (error) { // Handle the error } }, remove(name: string, options: CookieOptions) { try { cookieStore.set({ name, value: "", ...options }); } catch (error) { // Handle the error } }, }, } ); } export default async function Supabase() { const client = await createClerkSupabaseClient(); const { data, error } = await client.from("Addresses").select(); if (error) { return <p>Error: {JSON.stringify(error, null, 2)}</p>; } return ( <div> <h2>Addresses</h2> {!data ? ( <p>No addresses</p> ) : ( <ul> {data.map((address: any) => ( <li key={address.id}>{address.content}</li> ))} </ul> )} </div> ); }
-
Try out your application. When you visit the page with your component, you'll be required to sign in. Try creating and fetching content.
Next steps
- Try adding some custom claims to the JWT template in
app_metadata
oruser_metadata
Last updated on March 14, 2024