--- id: 'remix' title: 'Supabase Auth with Remix' description: 'Authentication helpers for loaders and actions in Remix.' sidebar_label: 'Remix' sitemapPriority: 0.5 --- We generally recommend using the new `@supabase/ssr` package instead of `auth-helpers`. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Check out the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) 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](https://egghead.io/courses/build-a-realtime-chat-app-with-remix-and-supabase-d36e2618) or [this GitHub repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix). ## Install the Remix helper library ```sh Terminal 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](https://supabase.com/dashboard/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/remix/.env.example). ```bash .env SUPABASE_URL=YOUR_SUPABASE_URL SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ``` ### Code Exchange route The `Code Exchange` route is required for the [server-side auth flow](https://supabase.com/docs/guides/auth/server-side-rendering) 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: ```jsx app/routes/auth.callback.jsx import { redirect } from '@remix-run/node' import { createServerClient } from '@supabase/auth-helpers-remix' export const loader = async ({ request }) => { const response = new Response() const url = new URL(request.url) const code = url.searchParams.get('code') if (code) { const supabaseClient = createServerClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, { request, response } ) await supabaseClient.auth.exchangeCodeForSession(code) } return redirect('/', { headers: response.headers, }) } ``` Create a new file at `app/routes/auth.callback.tsx` and populate with the following: ```tsx app/routes/auth.callback.tsx import { redirect } from '@remix-run/node' import { createServerClient } from '@supabase/auth-helpers-remix' import type { Database } from 'db_types' import type { LoaderFunctionArgs } from '@remix-run/node' export const loader = async ({ request }: LoaderFunctionArgs) => { const response = new Response() const url = new URL(request.url) const code = url.searchParams.get('code') if (code) { const supabaseClient = createServerClient( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { request, response } ) await supabaseClient.auth.exchangeCodeForSession(code) } return redirect('/', { headers: response.headers, }) } ``` > `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types). ## 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`. ```jsx 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 the `Loader` function. ```jsx import { json } from '@remix-run/node' // change this import to whatever runtime you are using import { createServerClient } from '@supabase/auth-helpers-remix' import type { LoaderFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using export const loader = async ({ request }: LoaderFunctionArgs) => { 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 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`. ```jsx 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 the `Action` function. ```jsx import { json } from '@remix-run/node' // change this import to whatever runtime you are using import { createServerClient } from '@supabase/auth-helpers-remix' import type { ActionFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using export const action = async ({ request }: ActionFunctionArgs) => { 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 the `Action` function. ## Session and user You can determine if a user is authenticated by checking their session using the `getSession` function. ```jsx const { data: { session }, } = await supabaseClient.auth.getSession() ``` The session contains a user property. This is the user metadata saved, unencoded, to the local storage medium. It's unverified and can be tampered by the user, so don't use it for authorization or sensitive purposes. ```jsx const user = session?.user ``` Or, if you need trusted user data, you can call the `getUser()` function, which retrieves the trusted user data by making a request to the Supabase Auth server. ```jsx 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. ```jsx app/root.jsx 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. ```jsx app/root.jsx const { env } = useLoaderData() ``` We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components. ```jsx app/root.jsx const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)) ``` And then we can share this instance across our application with Outlet Context. ```jsx app/root.jsx ``` ```tsx app/root.tsx export const loader = ({}: LoaderFunctionArgs) => { 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. ```tsx app/root.tsx const { env } = useLoaderData() ``` We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components. ```tsx app/root.tsx const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY) ) ``` And then we can share this instance across our application with Outlet Context. ```tsx app/root.tsx ``` ### 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. ```jsx app/root.jsx 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, } ) } ``` ```tsx app/root.tsx export const loader = async ({ request }: LoaderFunctionArgs) => { 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. ```jsx app/root.jsx 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, revalidate]) ``` ```tsx app/root.tsx 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 (event !== 'INITIAL_SESSION' && session?.access_token !== serverAccessToken) { // server and client are out of sync. revalidate() } }) return () => { subscription.unsubscribe() } }, [serverAccessToken, supabase, revalidate]) ``` Check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix) 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`](/docs/reference/javascript/auth-signup). ```jsx app/components/login.jsx 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', options: { redirectTo: 'http://localhost:3000/auth/callback', }, }) } const handleLogout = async () => { await supabase.auth.signOut() } return ( <> ) } ``` ```tsx app/components/login.tsx export default function Login() { const { supabase } = useOutletContext<{ supabase: SupabaseClient }>() const handleEmailLogin = async () => { await supabase.auth.signInWithPassword({ email: 'jon@supabase.com', password: 'password', }) } const handleGitHubLogin = async () => { await supabase.auth.signInWithOAuth({ provider: 'github', options: { redirectTo: 'http://localhost:3000/auth/callback', }, }) } const handleLogout = async () => { await supabase.auth.signOut() } return ( <> ) } ``` ### Subscribe to realtime events ```jsx app/routes/realtime.jsx 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
{JSON.stringify(posts, null, 2)}
} ```
```tsx app/routes/realtime.tsx 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' import type { SupabaseClient } from '@supabase/auth-helpers-remix' import type { Database } from 'db_types' type Post = Database['public']['Tables']['posts']['Row'] import type { LoaderFunctionArgs } from '@remix-run/node' export const loader = async ({ request }: LoaderFunctionArgs) => { 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<{ supabase: SupabaseClient }>() useEffect(() => { setPosts(serverPosts) }, [serverPosts]) useEffect(() => { const channel = supabase .channel('*') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) => setPosts([...posts, payload.new as Post]) ) .subscribe() return () => { supabase.removeChannel(channel) } }, [supabase, posts, setPosts]) return
{JSON.stringify(posts, null, 2)}
} ``` > `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types).
Ensure you have [enabled replication](https://supabase.com/dashboard/project/_/database/publications) 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](/docs/guides/auth/auth-helpers/remix#code-exchange-route) 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: ```jsx supabaseClient.auth.signUp({ email: 'jon@example.com', password: 'sup3rs3cur3', options: { emailRedirectTo: 'http://localhost:3000/auth/callback', }, }) ```