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#
npm 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.
1SUPABASE_URL=YOUR_SUPABASE_URL 2SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
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
.
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'
export const loader = async ({ request }) => {
const response = new Response()
// an empty response is required for the auth helpers
// to set cookies to manage auth
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
const { data } = await supabaseClient.from('test').select('*')
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ data },
{
headers: response.headers,
}
)
}
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
.
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'
export const action = async ({ request }) => {
const response = new Response()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
const { data } = await supabaseClient.from('test').select('*')
return json(
{ data },
{
headers: response.headers,
}
)
}
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.
const {
data: { session },
} = await supabaseClient.auth.getSession()
The session contains a user property.
const user = session?.user
Or, if you don't need the session, you can call the getUser()
function.
const {
data: { user },
} = 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.
export const loader = () => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
}
return json({ env })
}
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.
const { env } = useLoaderData()
We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.
const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))
And then we can share this instance across our application with Outlet Context.
<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.
export const loader = async ({ request }) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
}
const response = new Response()
const supabase = createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
request,
response,
})
const {
data: { session },
} = await supabase.auth.getSession()
return json(
{
env,
session,
},
{
headers: response.headers,
}
)
}
And then use the revalidator, inside the onAuthStateChange
hook.
const { env, session } = useLoaderData()
const { revalidate } = useRevalidator()
const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))
const serverAccessToken = session?.access_token
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== serverAccessToken) {
// server and client are out of sync.
revalidate()
}
})
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, supabase, fetcher])
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
.
export default function Login() {
const { supabase } = useOutletContext()
const handleEmailLogin = async () => {
await supabase.auth.signInWithPassword({
email: 'jon@supabase.com',
password: 'password',
})
}
const handleGitHubLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
})
}
const handleLogout = async () => {
await supabase.auth.signOut()
}
return (
<>
<button onClick={handleEmailLogin}>Email Login</button>
<button onClick={handleGitHubLogin}>GitHub Login</button>
<button onClick={handleLogout}>Logout</button>
</>
)
}
Subscribe to realtime events#
import { useLoaderData, useOutletContext } from '@remix-run/react'
import { createServerClient } from '@supabase/auth-helpers-remix'
import { json } from '@remix-run/node'
import { useEffect, useState } from 'react'
export const loader = async ({ request }) => {
const response = new Response()
const supabase = createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
request,
response,
})
const { data } = await supabase.from('posts').select()
return json({ serverPosts: data ?? [] }, { headers: response.headers })
}
export default function Index() {
const { serverPosts } = useLoaderData()
const [posts, setPosts] = useState(serverPosts)
const { supabase } = useOutletContext()
useEffect(() => {
setPosts(serverPosts)
}, [serverPosts])
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
setPosts([...posts, payload.new])
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase, posts, setPosts])
return <pre>{JSON.stringify(posts, null, 2)}</pre>
}
Ensure you have enabled replication on the table you are subscribing to.