* 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>
856 lines
21 KiB
Plaintext
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>
|