Supabase

Integrate Stack Auth with Supabase RLS

This guide shows how to integrate Stack Auth with Supabase row level security (RLS).

This guide only focuses on the RLS/JWT integration and does not sync user data between Supabase and Stack. You should use webhooks to achieve data sync.

Setup

Let’s create a sample table and some RLS policies to demonstrate how to integrate Stack Auth with Supabase RLS. You can apply the same logic to your own tables and policies.

1

Setup Supabase

First, let’s create a Supabase project, then go to the SQL Editor and create a new table with some sample data and RLS policies.

Supabase SQL Editor
1-- Create the 'data' table
2CREATE TABLE data (
3 id bigint PRIMARY KEY,
4 text text NOT NULL,
5 user_id UUID
6);
7
8-- Insert sample data
9INSERT INTO data (id, text, user_id) VALUES
10 (1, 'Everyone can see this', NULL),
11 (2, 'Only authenticated users can see this', NULL),
12 (3, 'Only user with specific id can see this', NULL);
13
14-- Enable Row Level Security
15ALTER TABLE data ENABLE ROW LEVEL SECURITY;
16
17-- Allow everyone to read the first row
18CREATE POLICY "Public read" ON "public"."data" TO public
19USING (id = 1);
20
21-- Allow authenticated users to read the second row
22CREATE POLICY "Authenticated access" ON "public"."data" TO authenticated
23USING (id = 2);
24
25-- Allow only the owner of the row to read it
26CREATE POLICY "User access" ON "public"."data" TO authenticated
27USING (id = 3 AND auth.uid() = user_id);
2

Setup a new Next.js project

Now let’s create a new Next.js project and install Stack Auth and Supabase client. (more details on Next.js setup, Stack Auth setup, and Supabase setup)

Terminal
$npx create-next-app@latest -e with-supabase stack-supabase
>cd stack-supabase
>npx @stackframe/init-stack@latest

Now copy the environment variables from the Supabase dashboard to the .env.local file:

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
  • SUPABASE_JWT_SECRET

Copy environment variables from the Stack dashboard to the .env.local file.

  • NEXT_PUBLIC_STACK_PROJECT_ID
  • NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY
  • STACK_SECRET_SERVER_KEY
3

Set up Supbase client

Now let’s create a server action that mints a supabase JWT with the Stack Auth user ID if the user is authenticated.

/utils/actions.ts
1'use server';
2
3import { stackServerApp } from "@/stack";
4import * as jose from "jose";
5
6export const getSupabaseJwt = async () => {
7 const user = await stackServerApp.getUser();
8
9 if (!user) {
10 return null;
11 }
12
13 const token = await new jose.SignJWT({
14 sub: user.id,
15 role: "authenticated",
16 })
17 .setProtectedHeader({ alg: "HS256" })
18 .setIssuedAt()
19 .setExpirationTime('1h')
20 .sign(new TextEncoder().encode(process.env.SUPABASE_JWT_SECRET));
21
22 return token;
23};

And now create a helper function to create a Supabase client with the JWT signed by the server action

/utils/supabase-client.ts
1import { createBrowserClient } from "@supabase/ssr";
2import { getSupabaseJwt } from "./actions";
3
4export const createSupabaseClient = () => {
5 return createBrowserClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
8 { accessToken: async () => await getSupabaseJwt() || "" }
9 );
10}
4

Fetch data from Supabase

Let’s create an example page that fetches data from Supabase and displays it.

/app/page.tsx
1'use client';
2
3import { createSupabaseClient } from "@/utils/supabase-client";
4import { useStackApp, useUser } from "@stackframe/stack";
5import Link from "next/link";
6import { useEffect, useState } from "react";
7
8export default function Page() {
9 const app = useStackApp();
10 const user = useUser();
11 const supabase = createSupabaseClient();
12 const [data, setData] = useState<null | any[]>(null);
13
14 useEffect(() => {
15 supabase.from("data").select().then(({ data }) => setData(data ?? []));
16 }, []);
17
18 const listContent = data === null ?
19 <p>Loading...</p> :
20 data.length === 0 ?
21 <p>No notes found</p> :
22 data.map((note) => <li key={note.id}>{note.text}</li>);
23
24 return (
25 <div>
26 {
27 user ?
28 <>
29 <p>You are signed in</p>
30 <p>User ID: {user.id}</p>
31 <Link href={app.urls.signOut}>Sign Out</Link>
32 </> :
33 <Link href={app.urls.signIn}>Sign In</Link>
34 }
35 <h3>Supabase data</h3>
36 <ul>{listContent}</ul>
37 </div>
38 )
39}

Now you should be able to compare the data you can view with an anonymous user, an authenticated user. You can also add your user Id to the row 3 of the Supabase table, and you should be able to see the row if and only if you are signed in with that user.

You can find the full example here on GitHub.