Supabase Auth with Remix
This submodule provides convenience helpers for implementing user authentication in Remix applications.
Install the Remix helper library#
1npm install @supabase/auth-helpers-remix
This library supports the following tooling versions:
- Remix:
>=1.7.2
Set up environment variables#
Retrieve your project URL and anon key in your project's API settings in the Dashboard to set up the following environment variables. For local development you can set them in a .env
file. See an example.
.envSUPABASE_URL=YOUR_SUPABASE_URL SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Loader#
Loader functions run on the server immediately before the component is rendered. They respond to all GET requests on a route. You can create an authenticated Supabase client by calling the createServerClient
function and passing it your SUPABASE_URL
, SUPABASE_ANON_KEY
, and a Request
and Response
.
1import { json } from '@remix-run/node' // change this import to whatever runtime you are using
2import { createServerClient } from '@supabase/auth-helpers-remix'
3
4export const loader = async ({ request }) => {
5 const response = new Response()
6 // an empty response is required for the auth helpers
7 // to set cookies to manage auth
8
9 const supabaseClient = createServerClient(
10 process.env.SUPABASE_URL,
11 process.env.SUPABASE_ANON_KEY,
12 { request, response }
13 )
14
15 const { data } = await supabaseClient.from('test').select('*')
16
17 // in order for the set-cookie header to be set,
18 // headers must be returned as part of the loader response
19 return json(
20 { data },
21 {
22 headers: response.headers,
23 }
24 )
25}
Supabase will set cookie headers to manage the user's auth session, therefore, the
response.headers
must be returned from theLoader
function.
Action#
Action functions run on the server and respond to HTTP requests to a route, other than GET - POST, PUT, PATCH, DELETE etc. You can create an authenticated Supabase client by calling the createServerClient
function and passing it your SUPABASE_URL
, SUPABASE_ANON_KEY
, and a Request
and Response
.
1import { json } from '@remix-run/node' // change this import to whatever runtime you are using
2import { createServerClient } from '@supabase/auth-helpers-remix'
3
4export const action = async ({ request }) => {
5 const response = new Response()
6
7 const supabaseClient = createServerClient(
8 process.env.SUPABASE_URL,
9 process.env.SUPABASE_ANON_KEY,
10 { request, response }
11 )
12
13 const { data } = await supabaseClient.from('test').select('*')
14
15 return json(
16 { data },
17 {
18 headers: response.headers,
19 }
20 )
21}
Supabase will set cookie headers to manage the user's auth session, therefore, the
response.headers
must be returned from theAction
function.
Session and User#
You can determine if a user is authenticated by checking their session using the getSession
function.
1const {
2 data: { session },
3} = await supabaseClient.auth.getSession()
The session contains a user property.
1const user = session?.user
This is the recommended way for accessing the logged in user. There is also a
getUser()
function but this does not refresh the session if it has expired.
Client-side#
In order to use the Supabase client in the browser - fetching data in useEffect
or subscribing to realtime events - we need to do a little more plumbing. Remix does not include a way to make environment variables available to the browser, so we need to pipe them through from a loader
function in our root.jsx
route and attach them to the window
.
app/root.jsx1export const loader = () => {
2 const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
3 return json({
4 env: {
5 SUPABASE_URL,
6 SUPABASE_ANON_KEY,
7 },
8 })
9}
These may not be stored in
process.env
for environments other than Node.
Next, we call the useLoaderData
hook in our component to get the env
object.
app/root.jsx1const { env } = useLoaderData()
And then, add a <script>
tag to attach these environment variables to the window
. This should be placed immediately before the <Scripts />
component in app/root.jsx
app/root.jsx1<script
2 dangerouslySetInnerHTML={{
3 __html: `window.env = ${JSON.stringify(env)}`,
4 }}
5/>
Full example for Node:
app/root.jsx1import { json } from '@remix-run/node' // change this import to whatever runtime you are using
2import {
3 Form,
4 Links,
5 LiveReload,
6 Meta,
7 Outlet,
8 Scripts,
9 ScrollRestoration,
10 useLoaderData,
11} from '@remix-run/react'
12import { createBrowserClient, createServerClient } from '@supabase/auth-helpers-remix'
13
14export const meta = () => ({
15 charset: 'utf-8',
16 title: 'New Remix App',
17 viewport: 'width=device-width,initial-scale=1',
18})
19
20export const loader = () => {
21 const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
22 return json({
23 env: {
24 SUPABASE_URL,
25 SUPABASE_ANON_KEY,
26 },
27 })
28}
29
30export default function App() {
31 const { env } = useLoaderData()
32
33 return (
34 <html lang="en">
35 <head>
36 <Meta />
37 <Links />
38 </head>
39 <body>
40 <Outlet />
41 <ScrollRestoration />
42 <script
43 dangerouslySetInnerHTML={{
44 __html: `window.env = ${JSON.stringify(env)}`,
45 }}
46 />47484950
51 )
52}
Now we can call createBrowserClient
in our components to fetch data client-side, or subscribe to realtime events - changes in the database.
Authentication#
Now that authentication is based on cookies, users can sign in and out server-side with actions.
Given this Remix <Form />
component.
1<Form method="post"> 2 <input type="text" name="email" /> 3 <input type="password" name="password" /> 4 <button type="submit">Go!</button> 5</Form>
Signing Up#
Any of the supported authentication strategies from supabase-js
will work server-side. This is how you would handle simple email
and password
auth.
1export const action = async ({ request }) => {
2 const { email, password } = Object.fromEntries(await request.formData())
3 const response = new Response()
4
5 const supabaseClient = createServerClient(
6 process.env.SUPABASE_URL,
7 process.env.SUPABASE_ANON_KEY,
8 { request, response }
9 )
10
11 const { data, error } = await supabaseClient.auth.signUp({
12 email,
13 password,
14 })
15
16 // in order for the set-cookie header to be set,
17 // headers must be returned as part of the loader response
18 return json(
19 { data, error },
20 {
21 headers: response.headers,
22 }
23 )
24}
Login#
Any of the supported authentication strategies from supabase-js
will work server-side. This is how you would handle simple email
and password
auth.
1export const action = async ({ request }) => {
2 const { email, password } = Object.fromEntries(await request.formData())
3 const response = new Response()
4
5 const supabaseClient = createServerClient(
6 process.env.SUPABASE_URL,
7 process.env.SUPABASE_ANON_KEY,
8 { request, response }
9 )
10
11 const { data, error } = await supabaseClient.auth.signInWithPassword({
12 email: String(loginEmail),
13 password: String(loginPassword),
14 })
15
16 // in order for the set-cookie header to be set,
17 // headers must be returned as part of the loader response
18 return json(
19 { data, error },
20 {
21 headers: response.headers,
22 }
23 )
24}
Logout#
1export const action = async ({ request }) => {
2 const { email, password } = Object.fromEntries(await request.formData())
3 const response = new Response()
4
5 const supabaseClient = createServerClient(
6 process.env.SUPABASE_URL,
7 process.env.SUPABASE_ANON_KEY,
8 { request, response }
9 )
10
11 const { error } = await supabaseClient.auth.signOut()
12
13 // in order for the set-cookie header to be set,
14 // headers must be returned as part of the loader response
15 return json(
16 { error },
17 {
18 headers: response.headers,
19 }
20 )
21}
Subscribe to realtime events#
1import { createBrowserClient } from '@supabase/auth-helpers-remix'
2import { useState, useEffect } from 'react'
3
4export default function SubscribeToRealtime() {
5 const [data, setData] = useState([])
6
7 useEffect(() => {
8 const supabaseClient = createBrowserClient(
9 window.env.SUPABASE_URL,
10 window.env.SUPABASE_ANON_KEY
11 )
12 const channel = supabaseClient
13 .channel('test')
14 .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'test' }, (payload) => {
15 setData((data) => [...data, payload.new])
16 })
17 .subscribe()
18
19 return () => {
20 supabaseClient.removeChannel(channel)
21 }
22 }, [session])
23
24 return <pre>{JSON.stringify({ data }, null, 2)}</pre>
25}
Note:
window.env
is not automatically populated by Remix. Check out the "Client-side" instructions above to configure this.
In this example we are listening to INSERT
events on the test
table. Anytime new rows are added to Supabase's test
table, our UI will automatically update new data.
Merge server and client state on realtime events#
1import { json, LoaderFunction } from '@remix-run/node';
2import { useLoaderData, useNavigate } from '@remix-run/react';
3import {
4 createServerClient,
5 createBrowserClient
6} from '@supabase/auth-helpers-remix';
7import { useEffect } from 'react';
8import { Database } from '../../db_types';
9
10// this route demonstrates how to subscribe to realtime updates
11// and synchronize data between server and client
12export const loader: LoaderFunction = async ({
13 request
14}: {
15 request: Request;
16}) => {
17 const response = new Response();
18 const supabaseClient = createServerClient<Database>(
19 process.env.SUPABASE_URL!,
20 process.env.SUPABASE_ANON_KEY!,
21 { request, response }
22 );
23
24 const {
25 data: { session }
26 } = await supabaseClient.auth.getSession();
27
28 const { data, error } = await supabaseClient.from('test').select('*');
29
30 if (error) {
31 throw error;
32 }
33
34 // in order for the set-cookie header to be set,
35 // headers must be returned as part of the loader response
36 return json(
37 { data, session },
38 {
39 headers: response.headers
40 }
41 );
42};
43
44export default function SubscribeToRealtime() {
45 const { data, session } = useLoaderData();
46 const navigate = useNavigate();
47
48 useEffect(() => {
49 // Note: window.env is not automatically populated by Remix
50 // Check out the [example in this repo](../root.tsx) or
51 // [Remix docs](https://remix.run/docs/en/v1/guides/envvars#browser-environment-variables) for more info
52 const supabaseClient = createBrowserClient<Database>(
53 window.env.SUPABASE_URL,
54 window.env.SUPABASE_ANON_KEY
55 );
56 // make sure you have enabled `Replication` for your table to receive realtime events
57 // https://supabase.com/docs/guides/database/replication
58 const channel = supabaseClient
59 .channel('test')
60 .on(
61 'postgres_changes',
62 { event: '*', schema: 'public', table: 'test' },
63 (payload: any) => {
64 // you could manually merge the `payload` with `data` here
65 // the `navigate` trick below causes all active loaders to be called again
66 // this handles inserts, updates and deletes, keeping everything in sync
67 // which feels more remix-y than manually merging state
68 // https://sergiodxa.com/articles/automatic-revalidation-in-remix
69 navigate('.', { replace: true });
70 }
71 )
72 .subscribe();
73
74 return () => {
75 supabaseClient.removeChannel(channel);
76 };
77 }, [session]);
78
79 return (
80 <div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
81 <pre>{JSON.stringify({ data }, null, 2)}</pre>
82 </div>
83 );
84}
Note:
window.env
is not automatically populated by Remix. Check out the "Client-side" instructions above to configure this.
Usage with TypeScript#
You can pass types that were generated with the Supabase CLI to the createServerClient
or createBrowserClient
functions to get enhanced type safety and auto completion:
Server-side#
1import { createServerClient } from '@supabase/auth-helpers-remix' 2import { Database } from '../../db_types' 3 4export const loader = async ({ request }) => { 5 const response = new Response() 6 7 const supabaseClient = createServerClient<Database>( 8 process.env.SUPABASE_URL, 9 process.env.SUPABASE_ANON_KEY, 10 { request, response } 11 ) 12}
Client-side#
1import { createBrowserClient } from '@supabase/auth-helpers-remix' 2import { Database } from '../../db_types' 3 4const supabaseClient = createBrowserClient<Database>( 5 process.env.SUPABASE_URL, 6 process.env.SUPABASE_ANON_KEY 7)
Server-side data fetching to OAuth APIs using provider_token
#
When using third-party auth providers, sessions are initiated with an additional provider_token
field which is persisted in the auth cookie and can be accessed within the session object. The provider_token
can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user.
1import { json, LoaderFunction, redirect } from '@remix-run/node'; // change this import to whatever runtime you are using
2import { useLoaderData } from '@remix-run/react';
3import { createServerClient, User } from '@supabase/auth-helpers-remix';
4import { Database } from '../../db_types';
5
6export const loader: LoaderFunction = async ({
7 request
8}: {
9 request: Request;
10}) => {
11 const response = new Response();
12
13 const supabaseClient = createServerClient<Database>(
14 process.env.SUPABASE_URL!,
15 process.env.SUPABASE_ANON_KEY!,
16 { request, response }
17 );
18
19 const {
20 data: { session }
21 } = await supabaseClient.auth.getSession();
22
23 if (!session) {
24 // there is no session, therefore, we are redirecting
25 // to the landing page. we still need to return
26 // response.headers to attach the set-cookie header
27 return redirect('/', {
28 headers: response.headers
29 });
30 }
31
32 // Retrieve provider_token & logged in user's third-party id from metadata
33 const { provider_token, user } = session;
34 const userId = user.user_metadata.user_name;
35
36 const allRepos = await (
37 await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
38 method: 'GET',
39 headers: {
40 Authorization: `token ${provider_token}`
41 }
42 })
43 ).json();
44
45 // in order for the set-cookie header to be set,
46 // headers must be returned as part of the loader response
47 return json(
48 { user, allRepos },
49 {
50 headers: response.headers
51 }
52 );
53};
54
55export default function ProtectedPage() {
56 // by fetching the user in the loader, we ensure it is available
57 // for first SSR render - no flashing of incorrect state
58 const { user, allRepos } = useLoaderData<{ user: User; allRepos: any }>();
59
60 return <pre>{JSON.stringify({ user, allRepos }, null, 2)}</pre>;
61}