Files
supabase/apps/docs/content/guides/auth/auth-helpers/remix.mdx
Charis 47705a8968 chore: replace all supabase urls with relative urls (#38537)
* fix: rewrite relative URLs when syncing to GitHub discussion

Relative URLs back to supabse.com won't work in GitHub discussions, so
rewrite them back to absolute URLs starting with https://supabase.com

* fix: replace all supabase urls with relative urls

* chore: add linting for relative urls

* chore: bump linter version

* Prettier

---------

Co-authored-by: Chris Chinchilla <chris.ward@supabase.io>
2025-09-09 12:54:33 +00:00

856 lines
21 KiB
Plaintext

---
id: 'remix'
title: 'Supabase Auth with Remix'
description: 'Authentication helpers for loaders and actions in Remix.'
sidebar_label: 'Remix'
sitemapPriority: 0.5
---
<Admonition type="caution">
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.
</Admonition>
<Accordion
type="default"
openBehaviour="multiple"
chevronAlign="right"
justified
size="medium"
className="text-foreground-light border-b mt-8 pb-2"
>
<AccordionItem
header="See legacy docs"
id="legacy-docs"
>
This submodule provides convenience helpers for implementing user authentication in Remix applications.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Viaed7XWCY8"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<Admonition type="tip">
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).
</Admonition>
## 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](/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_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
```
### Code Exchange route
The `Code Exchange` route is required for the [server-side auth flow](/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.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
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_PUBLISHABLE_KEY,
{ request, response }
)
await supabaseClient.auth.exchangeCodeForSession(code)
}
return redirect('/', {
headers: response.headers,
})
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
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<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_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).
</TabPanel>
</Tabs>
## 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_PUBLISHABLE_KEY`, and a `Request` and `Response`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```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_PUBLISHABLE_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,
}
)
}
```
<Admonition type="tip">
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function.
</Admonition>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```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_PUBLISHABLE_KEY!,
{ request, response }
)
const { data } = await supabaseClient.from('test').select('*')
return json(
{ data },
{
headers: response.headers,
}
)
}
```
<Admonition type="tip">
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function.
</Admonition>
</TabPanel>
</Tabs>
### 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_PUBLISHABLE_KEY`, and a `Request` and `Response`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```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_PUBLISHABLE_KEY,
{ request, response }
)
const { data } = await supabaseClient.from('test').select('*')
return json(
{ data },
{
headers: response.headers,
}
)
}
```
<Admonition type="tip">
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function.
</Admonition>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```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_PUBLISHABLE_KEY!,
{ request, response }
)
const { data } = await supabaseClient.from('test').select('*')
return json(
{ data },
{
headers: response.headers,
}
)
}
```
<Admonition type="tip">
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function.
</Admonition>
</TabPanel>
</Tabs>
## 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.
<$Partial path="get_session_warning.mdx" />
```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.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```jsx app/root.jsx
export const loader = () => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY,
}
return json({ env })
}
```
<Admonition type="tip">
These may not be stored in `process.env` for environments other than Node.
</Admonition>
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_PUBLISHABLE_KEY)
)
```
And then we can share this instance across our application with Outlet Context.
```jsx app/root.jsx
<Outlet context={{ supabase }} />
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx app/root.tsx
export const loader = ({}: LoaderFunctionArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY!,
}
return json({ env })
}
```
<Admonition type="tip">
These may not be stored in `process.env` for environments other than Node.
</Admonition>
Next, we call the `useLoaderData` hook in our component to get the `env` object.
```tsx app/root.tsx
const { env } = useLoaderData<typeof loader>()
```
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<Database>(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY)
)
```
And then we can share this instance across our application with Outlet Context.
```tsx app/root.tsx
<Outlet context={{ supabase }} />
```
</TabPanel>
</Tabs>
### 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.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```jsx app/root.jsx
export const loader = async ({ request }) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY,
}
const response = new Response()
const supabase = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_PUBLISHABLE_KEY,
{
request,
response,
}
)
const {
data: { session },
} = await supabase.auth.getSession()
return json(
{
env,
session,
},
{
headers: response.headers,
}
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx app/root.tsx
export const loader = async ({ request }: LoaderFunctionArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY!,
}
const response = new Response()
const supabase = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!,
{
request,
response,
}
)
const {
data: { session },
} = await supabase.auth.getSession()
return json(
{
env,
session,
},
{
headers: response.headers,
}
)
}
```
</TabPanel>
</Tabs>
And then use the revalidator, inside the `onAuthStateChange` hook.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```jsx app/root.jsx
const { env, session } = useLoaderData()
const { revalidate } = useRevalidator()
const [supabase] = useState(() =>
createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_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])
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx app/root.tsx
const { env, session } = useLoaderData<typeof loader>()
const { revalidate } = useRevalidator()
const [supabase] = useState(() =>
createBrowserClient<Database>(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_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])
```
</TabPanel>
</Tabs>
<Admonition type="tip">
Check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix) for full implementation example
</Admonition>
### 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).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```jsx app/components/login.jsx
export default function Login() {
const { supabase } = useOutletContext()
const handleEmailLogin = async () => {
await supabase.auth.signInWithPassword({
email: 'valid.email@supabase.io',
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 (
<>
<button onClick={handleEmailLogin}>Email Login</button>
<button onClick={handleGitHubLogin}>GitHub Login</button>
<button onClick={handleLogout}>Logout</button>
</>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx app/components/login.tsx
export default function Login() {
const { supabase } = useOutletContext<{ supabase: SupabaseClient<Database> }>()
const handleEmailLogin = async () => {
await supabase.auth.signInWithPassword({
email: 'valid.email@supabase.io',
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 (
<>
<button onClick={handleEmailLogin}>Email Login</button>
<button onClick={handleGitHubLogin}>GitHub Login</button>
<button onClick={handleLogout}>Logout</button>
</>
)
}
```
</TabPanel>
</Tabs>
### Subscribe to realtime events
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```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_PUBLISHABLE_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>
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```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<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!,
{
request,
response,
}
)
const { data } = await supabase.from('posts').select()
return json({ serverPosts: data ?? [] }, { headers: response.headers })
}
export default function Index() {
const { serverPosts } = useLoaderData<typeof loader>()
const [posts, setPosts] = useState(serverPosts)
const { supabase } = useOutletContext<{ supabase: SupabaseClient<Database> }>()
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 <pre>{JSON.stringify(posts, null, 2)}</pre>
}
```
> `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types).
</TabPanel>
</Tabs>
<Admonition type="tip">
Ensure you have [enabled replication](/dashboard/project/_/database/publications) on the table you are subscribing to.
</Admonition>
## 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: 'valid.email@supabase.io',
password: 'sup3rs3cur3',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback',
},
})
```
</AccordionItem>
</Accordion>