Home

Supabase Auth with Remix

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
})