|
|
|
@@ -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>
|
|
|
|
|
}
|
|
|
|
|
```
|