Home

Supabase Auth with Remix

danger

The auth-helpers packages have been deprecated in favor of the new ssr package. This takes the core concepts of the Auth Helpers package and makes them available to any server language or framework. Check out the migration doc to learn more.

This submodule provides convenience helpers for implementing user authentication in Remix applications.

For a complete implementation example, check out this free egghead course or this GitHub repo.

Install the Remix helper library#

Terminal

_10
npm install @supabase/auth-helpers-remix @supabase/supabase-js

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.

.env

_10
SUPABASE_URL=YOUR_SUPABASE_URL
_10
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Code Exchange Route#

The Code Exchange route is required for the server-side auth flow implemented by the Remix Auth Helpers. It exchanges an auth code for the user's session, which is set as a cookie for future requests made to Supabase.

Create a new file at app/routes/auth.callback.jsx and populate with the following:

app/routes/auth.callback.jsx

_21
import { redirect } from '@remix-run/node'
_21
import { createServerClient } from '@supabase/auth-helpers-remix'
_21
_21
export const loader = async ({ request }) => {
_21
const response = new Response()
_21
const url = new URL(request.url)
_21
const code = url.searchParams.get('code')
_21
_21
if (code) {
_21
const supabaseClient = createServerClient(
_21
process.env.SUPABASE_URL,
_21
process.env.SUPABASE_ANON_KEY,
_21
{ request, response }
_21
)
_21
await supabaseClient.auth.exchangeCodeForSession(code)
_21
}
_21
_21
return redirect('/', {
_21
headers: response.headers,
_21
})
_21
}

Server-side#

The Supabase client can now be used server-side - in loaders and actions - by calling the createServerClient function.

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.


_25
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
_25
import { createServerClient } from '@supabase/auth-helpers-remix'
_25
_25
export const loader = async ({ request }) => {
_25
const response = new Response()
_25
// an empty response is required for the auth helpers
_25
// to set cookies to manage auth
_25
_25
const supabaseClient = createServerClient(
_25
process.env.SUPABASE_URL,
_25
process.env.SUPABASE_ANON_KEY,
_25
{ request, response }
_25
)
_25
_25
const { data } = await supabaseClient.from('test').select('*')
_25
_25
// in order for the set-cookie header to be set,
_25
// headers must be returned as part of the loader response
_25
return json(
_25
{ data },
_25
{
_25
headers: response.headers,
_25
}
_25
)
_25
}

Supabase will set cookie headers to manage the user's auth session, therefore, the response.headers must be returned from the Loader 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.


_21
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
_21
import { createServerClient } from '@supabase/auth-helpers-remix'
_21
_21
export const action = async ({ request }) => {
_21
const response = new Response()
_21
_21
const supabaseClient = createServerClient(
_21
process.env.SUPABASE_URL,
_21
process.env.SUPABASE_ANON_KEY,
_21
{ request, response }
_21
)
_21
_21
const { data } = await supabaseClient.from('test').select('*')
_21
_21
return json(
_21
{ data },
_21
{
_21
headers: response.headers,
_21
}
_21
)
_21
}

Supabase will set cookie headers to manage the user's auth session, therefore, the response.headers must be returned from the Action function.

Session and User#

You can determine if a user is authenticated by checking their session using the getSession function.


_10
const {
_10
data: { session },
_10
} = await supabaseClient.auth.getSession()

The session contains a user property.


_10
const user = session?.user

Or, if you don't need the session, you can call the getUser() function.


_10
const {
_10
data: { user },
_10
} = await supabaseClient.auth.getUser()

Client-side#

We still need to use Supabase client-side for things like authentication and realtime subscriptions. Anytime we use Supabase client-side it needs to be a single instance.

Creating a singleton Supabase client#

Since our environment variables are not available client-side, we need to plumb them through from the loader.

app/root.jsx

_10
export const loader = () => {
_10
const env = {
_10
SUPABASE_URL: process.env.SUPABASE_URL,
_10
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
_10
}
_10
_10
return json({ env })
_10
}

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.jsx

_10
const { env } = useLoaderData()

We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.

app/root.jsx

_10
const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))

And then we can share this instance across our application with Outlet Context.

app/root.jsx

_10
<Outlet context={{ supabase }} />

Syncing server and client state#

Since authentication happens client-side, we need to tell Remix to re-call all active loaders when the user signs in or out.

Remix provides a hook useRevalidator that can be used to revalidate all loaders on the current route.

Now to determine when to submit a post request to this action, we need to compare the server and client state for the user's access token.

Let's pipe that through from our loader.

app/root.jsx

_27
export const loader = async ({ request }) => {
_27
const env = {
_27
SUPABASE_URL: process.env.SUPABASE_URL,
_27
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
_27
}
_27
_27
const response = new Response()
_27
_27
const supabase = createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
_27
request,
_27
response,
_27
})
_27
_27
const {
_27
data: { session },
_27
} = await supabase.auth.getSession()
_27
_27
return json(
_27
{
_27
env,
_27
session,
_27
},
_27
{
_27
headers: response.headers,
_27
}
_27
)
_27
}

And then use the revalidator, inside the onAuthStateChange hook.

app/root.jsx

_21
const { env, session } = useLoaderData()
_21
const { revalidate } = useRevalidator()
_21
_21
const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))
_21
_21
const serverAccessToken = session?.access_token
_21
_21
useEffect(() => {
_21
const {
_21
data: { subscription },
_21
} = supabase.auth.onAuthStateChange((event, session) => {
_21
if (session?.access_token !== serverAccessToken) {
_21
// server and client are out of sync.
_21
revalidate()
_21
}
_21
})
_21
_21
return () => {
_21
subscription.unsubscribe()
_21
}
_21
}, [serverAccessToken, supabase, revalidate])

Check out this repo for full implementation example

Authentication#

Now we can use our outlet context to access our single instance of Supabase and use any of the supported authentication strategies from supabase-js.

app/components/login.jsx

_31
export default function Login() {
_31
const { supabase } = useOutletContext()
_31
_31
const handleEmailLogin = async () => {
_31
await supabase.auth.signInWithPassword({
_31
email: 'jon@supabase.com',
_31
password: 'password',
_31
})
_31
}
_31
_31
const handleGitHubLogin = async () => {
_31
await supabase.auth.signInWithOAuth({
_31
provider: 'github',
_31
options: {
_31
redirectTo: 'http://localhost:3000/auth/callback',
_31
},
_31
})
_31
}
_31
_31
const handleLogout = async () => {
_31
await supabase.auth.signOut()
_31
}
_31
_31
return (
_31
<>
_31
<button onClick={handleEmailLogin}>Email Login</button>
_31
<button onClick={handleGitHubLogin}>GitHub Login</button>
_31
<button onClick={handleLogout}>Logout</button>
_31
</>
_31
)
_31
}

Subscribe to realtime events#

app/routes/realtime.jsx

_41
import { useLoaderData, useOutletContext } from '@remix-run/react'
_41
import { createServerClient } from '@supabase/auth-helpers-remix'
_41
import { json } from '@remix-run/node'
_41
import { useEffect, useState } from 'react'
_41
_41
export const loader = async ({ request }) => {
_41
const response = new Response()
_41
const supabase = createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
_41
request,
_41
response,
_41
})
_41
_41
const { data } = await supabase.from('posts').select()
_41
_41
return json({ serverPosts: data ?? [] }, { headers: response.headers })
_41
}
_41
_41
export default function Index() {
_41
const { serverPosts } = useLoaderData()
_41
const [posts, setPosts] = useState(serverPosts)
_41
const { supabase } = useOutletContext()
_41
_41
useEffect(() => {
_41
setPosts(serverPosts)
_41
}, [serverPosts])
_41
_41
useEffect(() => {
_41
const channel = supabase
_41
.channel('*')
_41
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
_41
setPosts([...posts, payload.new])
_41
)
_41
.subscribe()
_41
_41
return () => {
_41
supabase.removeChannel(channel)
_41
}
_41
}, [supabase, posts, setPosts])
_41
_41
return <pre>{JSON.stringify(posts, null, 2)}</pre>
_41
}

Ensure you have enabled replication on the table you are subscribing to.

Migration Guide#

Migrating to v0.2.0#

PKCE Auth Flow

PKCE is the new server-side auth flow implemented by the Remix Auth Helpers. It requires a new loader route for /auth/callback that exchanges an auth code for the user's session.

Check the Code Exchange Route steps above to implement this route.

Authentication

For authentication methods that have a redirectTo or emailRedirectTo, this must be set to this new code exchange API Route - /api/auth/callback. This is an example with the signUp function:


_10
supabaseClient.auth.signUp({
_10
email: 'jon@example.com',
_10
password: 'sup3rs3cur3',
_10
options: {
_10
emailRedirectTo: 'http://localhost:3000/auth/callback',
_10
},
_10
})