Merge pull request #9959 from supabase/docs/add-remix-auth-helper-docs

Docs: add remix auth helper docs
This commit is contained in:
Jon Meyers authored and GitHub committed 2022-10-31 21:23:29 +11:00
commit 8bc90fcdfc
7 files changed
+938

No files matched your search

@@ -46,6 +46,18 @@ A collection of framework-specific Auth utilities for working with Supabase.
style={{ height: '100%' }}
/>
</div>
{/* Remix */}
<div class="col col--4">
<ButtonCard
class="card"
to={useBaseUrl('/guides/auth/auth-helpers/remix')}
title={'Remix'}
description={
'Helpers for authenticating users in Remix applications.'
}
style={{ height: '100%' }}
/>
</div>
</div>
</div>
@@ -0,0 +1,910 @@
---
id: remix
title: Supabase Auth with Remix
description: Authentication helpers for loaders and actions in Remix.
sidebar_label: 'Remix'
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
This submodule provides convenience helpers for implementing user authentication in Remix applications.
## Install the Remix helper library
<Tabs
groupId="install"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'Yarn', value: 'yarn'},
]}>
<TabItem value="npm">
```sh
npm install @supabase/auth-helpers-remix
```
This library supports the following tooling versions:
- Remix: `>=1.7.2`
</TabItem>
<TabItem value="yarn">
```sh
yarn add @supabase/auth-helpers-remix
```
This library supports the following tooling versions:
- Remix: `>=1.7.2`
</TabItem>
</Tabs>
## Set up environment variables
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/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 title=".env"
SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
## Loader
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
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 `createSupabaseClient` 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 { createSupabaseClient } 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 = createSupabaseClient(
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.
</TabItem>
<TabItem value="ts">
Loader functions run on the server immediately before the component is rendered. You can create an authenticated Supabase client by calling the `createSupabaseClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`.
```jsx
import { LoaderFunction, json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createSupabaseClient } from '@supabase/auth-helpers-remix'
export const loader: LoaderFunction = async ({
request,
}: {
request: Request,
}) => {
const response = new Response()
const supabaseClient = createSupabaseClient(
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.
</TabItem>
</Tabs>
## Action
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
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 `createSupabaseClient` 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 { createSupabaseClient } from '@supabase/auth-helpers-remix'
export const action = async ({ request }) => {
const response = new Response()
const supabaseClient = createSupabaseClient(
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.
</TabItem>
<TabItem value="ts">
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 `createSupabaseClient` function and passing it your `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and a `Request` and `Response`.
```jsx
import { ActionFunction, json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createSupabaseClient } from '@supabase/auth-helpers-remix'
export const action: ActionFunction = async ({
request,
}: {
request: Request,
}) => {
const response = new Response()
const supabaseClient = createSupabaseClient(
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.
</TabItem>
</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.
```jsx
const user = session?.user
```
> This is the recommended way for accessing the logged in user. There is also a `getUser()` function but this does not refresh the session if it has expired.
## Client-side
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}
>
<TabItem value="js">
In order to use the Supabase client in the browser - fetching data in `useEffect` or subscribing to realtime events - we need to do a little more plumbing. Remix does not include a way to make environment variables available to the browser, so we need to pipe them through from a `loader` function in our `root.jsx` route and attach them to the `window`.
```jsx title="app/root.jsx"
export const loader = () => {
const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
return json({
env: {
SUPABASE_URL,
SUPABASE_ANON_KEY,
},
})
}
```
> 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 title="app/root.jsx"
const { env } = useLoaderData()
```
And then, add a `<script>` tag to attach these environment variables to the `window`. This should be placed immediately before the `<Scripts />` component in `app/root.jsx`
```jsx title="app/root.jsx"
<script
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>
```
Full example for Node:
```jsx title="app/root.jsx"
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import {
Form,
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from '@remix-run/react'
import { createSupabaseClient } from '@supabase/auth-helpers-remix'
export const meta = () => ({
charset: 'utf-8',
title: 'New Remix App',
viewport: 'width=device-width,initial-scale=1',
})
export const loader = () => {
const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
return json({
env: {
SUPABASE_URL,
SUPABASE_ANON_KEY,
},
})
}
export default function App() {
const { env } = useLoaderData()
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<script
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>
<Scripts />
<LiveReload />
</body>
</html>
)
}
```
Now we can call `createBrowserClient` in our components to fetch data client-side, or subscribe to realtime events - changes in the database.
</TabItem>
<TabItem value="ts">
In order to use the Supabase client in the browser - fetching data in `useEffect` or subscribing to realtime events - we need to do a little more plumbing. Remix does not include a way to make environment variables available to the browser, so we need to pipe them through from a `loader` function in our `root.tsx` route and attach them to the `window`.
```jsx title="app/root.tsx"
export const loader = () => {
const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
return json({
env: {
SUPABASE_URL,
SUPABASE_ANON_KEY,
},
})
}
```
> 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 title="app/root.tsx"
const { env } = useLoaderData()
```
And then, add a `<script>` tag to attach these environment variables to the `window`. This should be placed immediately before the `<Scripts />` component in `app/root.tsx`
```jsx title="app/root.tsx"
<script
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>
```
Full example for Node:
```jsx title="app/root.tsx"
import { json, MetaFunction, LoaderFunction } from '@remix-run/node' // change this import to whatever runtime you are using
import {
Form,
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from '@remix-run/react'
import { createSupabaseClient } from '@supabase/auth-helpers-remix'
export const meta: MetaFunction = () => ({
charset: 'utf-8',
title: 'New Remix App',
viewport: 'width=device-width,initial-scale=1',
})
export const loader: LoaderFunction = () => {
const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
return json({
env: {
SUPABASE_URL,
SUPABASE_ANON_KEY,
},
})
}
export default function App() {
const { env } = useLoaderData()
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<script
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>
<Scripts />
<LiveReload />
</body>
</html>
)
}
```
Now we can call `createBrowserClient` in our components to fetch data client-side, or subscribe to realtime events - changes in the database.
</TabItem>
</Tabs>
## Authentication
Now that authentication is based on cookies, users can sign in and out server-side with actions.
Given this Remix `<Form />` component.
```jsx
<Form method="post">
<input type="text" name="email" />
<input type="password" name="password" />
<button type="submit">Go!</button>
</Form>
```
### Signing Up
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
Any of the [supported authentication strategies from `supabase-js`](https://supabase.com/docs/reference/javascript/auth-signup) will work server-side. This is how you would handle simple `email` and `password` auth.
```jsx
export const action = async ({ request }) => {
const { email, password } = Object.fromEntries(await request.formData())
const response = new Response()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
const { data, error } = await supabaseClient.auth.signUp({
email,
password,
})
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ data, error },
{
headers: response.headers,
}
)
}
```
</TabItem>
<TabItem value="ts">
Any of the [supported authentication strategies from `supabase-js`](https://supabase.com/docs/reference/javascript/auth-signup) will work server-side. This is how you would handle simple `email` and `password` auth.
```jsx
export const action: ActionFunction = async ({
request
}: {
request: Request;
}) => {
const { email, password } = Object.fromEntries(await request.formData());
const response = new Response();
const supabaseClient = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ request, response }
);
const { data, error } = await supabaseClient.auth.signUp({
email: String(registerEmail),
password: String(registerPassword)
});
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ data, error },
{
headers: response.headers
}
);
};
```
</TabItem>
</Tabs>
### Login
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
Any of the [supported authentication strategies from `supabase-js`](https://supabase.com/docs/reference/javascript/auth-signinwithpassword) will work server-side. This is how you would handle simple `email` and `password` auth.
```jsx
export const action = async ({ request }) => {
const { email, password } = Object.fromEntries(await request.formData())
const response = new Response()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
const { data, error } = await supabaseClient.auth.signInWithPassword({
email: String(loginEmail),
password: String(loginPassword),
})
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ data, error },
{
headers: response.headers,
}
)
}
```
</TabItem>
<TabItem value="ts">
Any of the [supported authentication strategies from `supabase-js`](https://supabase.com/docs/reference/javascript/auth-signinwithpassword) will work server-side. This is how you would handle simple `email` and `password` auth.
```jsx
export const action: ActionFunction = async ({
request
}: {
request: Request;
}) => {
const { email, password } = Object.fromEntries(await request.formData());
const response = new Response();
const supabaseClient = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ request, response }
);
const { data, error } = await supabaseClient.auth.signInWithPassword({
email: String(loginEmail),
password: String(loginPassword)
});
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ data, error },
{
headers: response.headers
}
);
};
```
</TabItem>
</Tabs>
### Logout
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
```jsx
export const action = async ({ request }) => {
const { email, password } = Object.fromEntries(await request.formData())
const response = new Response()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
const { error } = await supabaseClient.auth.signOut()
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ error },
{
headers: response.headers,
}
)
}
```
</TabItem>
<TabItem value="ts">
```jsx
export const action: ActionFunction = async ({
request
}: {
request: Request;
}) => {
const { email, password } = Object.fromEntries(await request.formData());
const response = new Response();
const supabaseClient = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ request, response }
);
const { error } = await supabaseClient.auth.signOut();
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ error },
{
headers: response.headers
}
);
};
```
</TabItem>
</Tabs>
## Subscribe to realtime events
```jsx
import { createBrowserClient } from '@supabase/auth-helpers-remix'
import { useState, useEffect } from 'react'
export default function SubscribeToRealtime() {
const [data, setData] = useState([])
useEffect(() => {
const supabaseClient = createBrowserClient(
window.env.SUPABASE_URL,
window.env.SUPABASE_ANON_KEY
)
const channel = supabaseClient
.channel('test')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'test' },
(payload) => {
setData((data) => [...data, payload.new])
}
)
.subscribe()
return () => {
supabaseClient.removeChannel(channel)
}
}, [session])
return <pre>{JSON.stringify({ data }, null, 2)}</pre>
}
```
> Note: `window.env` is not automatically populated by Remix. Check out the "Client-side" instructions above to configure this.
In this example we are listening to `INSERT` events on the `test` table. Anytime new rows are added to Supabase's `test` table, our UI will automatically update new data.
## Merge server and client state on realtime events
```jsx
import { json, LoaderFunction } from '@remix-run/node';
import { useLoaderData, useNavigate } from '@remix-run/react';
import {
createServerClient,
createBrowserClient
} from '@supabase/auth-helpers-remix';
import { useEffect } from 'react';
import { Database } from '../../db_types';
// this route demonstrates how to subscribe to realtime updates
// and synchronize data between server and client
export const loader: LoaderFunction = async ({
request
}: {
request: Request;
}) => {
const response = new Response();
const supabaseClient = createServerClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ request, response }
);
const {
data: { session }
} = await supabaseClient.auth.getSession();
const { data, error } = await supabaseClient.from('test').select('*');
if (error) {
throw error;
}
// in order for the set-cookie header to be set,
// headers must be returned as part of the loader response
return json(
{ data, session },
{
headers: response.headers
}
);
};
export default function SubscribeToRealtime() {
const { data, session } = useLoaderData();
const navigate = useNavigate();
useEffect(() => {
// Note: window.env is not automatically populated by Remix
// Check out the [example in this repo](../root.tsx) or
// [Remix docs](https://remix.run/docs/en/v1/guides/envvars#browser-environment-variables) for more info
const supabaseClient = createBrowserClient<Database>(
window.env.SUPABASE_URL,
window.env.SUPABASE_ANON_KEY
);
// make sure you have enabled `Replication` for your table to receive realtime events
// https://supabase.com/docs/guides/database/replication
const channel = supabaseClient
.channel('test')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'test' },
(payload: any) => {
// you could manually merge the `payload` with `data` here
// the `navigate` trick below causes all active loaders to be called again
// this handles inserts, updates and deletes, keeping everything in sync
// which feels more remix-y than manually merging state
// https://sergiodxa.com/articles/automatic-revalidation-in-remix
navigate('.', { replace: true });
}
)
.subscribe();
return () => {
supabaseClient.removeChannel(channel);
};
}, [session]);
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
<pre>{JSON.stringify({ data }, null, 2)}</pre>
</div>
);
}
```
> Note: `window.env` is not automatically populated by Remix. Check out the "Client-side" instructions above to configure this.
## Usage with TypeScript
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the `createServerClient` or `createBrowserClient` functions to get enhanced type safety and auto completion:
### Server-side
```ts
import { createServerClient } from '@supabase/auth-helpers-remix'
import { Database } from '../../db_types'
export const loader = async ({ request }) => {
const response = new Response()
const supabaseClient = createServerClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
}
```
### Client-side
```ts
import { createBrowserClient } from '@supabase/auth-helpers-remix'
import { Database } from '../../db_types'
const supabaseClient = createBrowserClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
)
```
## Server-side data fetching to OAuth APIs using `provider_token`
When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted as an HTTPOnly cookie upon logging in to enabled usage on the server side. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. In the following example, we fetch the user's full profile from the third-party API using their id and auth token:
```jsx
import { User, createServerClient } from '@supabase/auth-helpers-remix'
import { ActionFunction, json, redirect } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'
interface Profile {
/* ... */
}
export const loader: LoaderFunction = async ({
request,
}: {
request: Request,
}) => {
const response = new Response()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
const {
data: { session },
} = await supabaseClient.auth.getSession()
const user = session?.user
if (!user) {
// there is no user, therefore, we are redirecting
// to the landing page. we still need to return
// response.headers to attach the set-cookie header
return redirect('/', {
headers: response.headers,
})
}
// Retrieve provider_token from cookies
const provider_token = request.headers.get('sb-provider-token')
if (!provider_token) {
// there is no provider_token, therefore, we are redirecting
// to the landing page. we still need to return
// response.headers to attach the set-cookie header
return redirect('/', {
headers: response.headers,
})
}
// Get logged in user's third-party id from metadata
const userId = user?.user_metadata.provider_id
const allRepos = await (
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
method: 'GET',
headers: {
Authorization: `token ${provider_token}`,
},
})
).json()
// in order for the set-cookie header to be set,
// headers must be returned as part of the action response
return json(
{ allRepos, user },
{
headers: response.headers,
}
)
}
export default function Page() {
const { allRepos, user } = useLoaderData()
return <pre>{JSON.stringify({ allRepos, user }, null, 2)}</pre>
}
```
+1
View File
@@ -165,6 +165,7 @@ Both Postgres and the Supabase Platform are production-ready. Some tools we offe
| Auth | Passwordless | `beta` |
| Auth | Next.js Auth Helpers | `alpha` |
| Auth | SvelteKit Auth Helpers | `alpha` |
| Auth | Remix Auth Helpers | `alpha` |
| Management API | | `beta` |
| CLI | | `beta` |
| Client Library: JavaScript | | `GA` |
+1
View File
@@ -93,6 +93,7 @@ const sidebars = {
'guides/auth/auth-helpers/auth-ui',
'guides/auth/auth-helpers/nextjs',
'guides/auth/auth-helpers/sveltekit',
'guides/auth/auth-helpers/remix',
],
},
{
@@ -123,6 +123,7 @@ export const menuItems: NavMenu = {
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui', items: [] },
{ name: 'Next.js', url: '/guides/auth/auth-helpers/nextjs', items: [] },
{ name: 'SvelteKit', url: '/guides/auth/auth-helpers/sveltekit', items: [] },
{ name: 'Remix', url: '/guides/auth/auth-helpers/remix', items: [] },
],
},
{
@@ -46,6 +46,18 @@ A collection of framework-specific Auth utilities for working with Supabase.
style={{ height: '100%' }}
/>
</div>
{/* Remix */}
<div class="col col--4">
<ButtonCard
class="card"
to={useBaseUrl('/guides/auth/auth-helpers/remix')}
title={'Remix'}
description={
'Helpers for authenticating users in Remix applications.'
}
style={{ height: '100%' }}
/>
</div>
</div>
</div>
+1
View File
@@ -165,6 +165,7 @@ Both Postgres and the Supabase Platform are production-ready. Some tools we offe
| Auth | Passwordless | `beta` |
| Auth | Next.js Auth Helpers | `alpha` |
| Auth | SvelteKit Auth Helpers | `alpha` |
| Auth | Remix Auth Helpers | `alpha` |
| Admin API | | `beta` |
| CLI | | `beta` |
| Client Library: JavaScript | | `GA` |