mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Closes DOCS-1313 Closes FDBKIN-4573 Closes FDBKIN-15214 Closes FDBKIN-10628 ## Problem Four asks come up repeatedly in feedback intake. The Eval is green and this feedback cannot be included in the Eval. Using the Evals work as an excuse to action on the feedback. 😄 Readers can't tell which auth call verifies a token and which only reads stored state. They don't know that the response the cookies were written to is the response they have to return, because that only ever existed as a code comment. Nobody is warned that refreshing in two places burns a single-use refresh token, which surfaces as users being signed out at random. And nothing in `apps/docs` says `proxy.ts` is Next.js 16 and later, so a reader on 15 writes a file the framework never calls. ## Solution - Add the fact that `getClaims()` refreshes a session close to expiring before it verifies. It was only in the typedoc remarks, and it is what makes the double refresh warning make sense. - Say that `setAll` rebuilds `supabaseResponse` on every write, so a response built earlier is stale, and show how to copy the cookies onto a different one. - Warn that a second refresh outside the reuse window revokes the session, linking refresh token reuse detection. - Note that `proxy.ts` is Next.js 16 and later, and that the file is `middleware.ts` before that. - Name the file in the proxy fence in `examples/prompts/nextjs-supabase-auth.md`, which gave agents the export name and no path. The auth methods partial is shared by five other pages, so that first change surfaces there too. ## Manual testing 1. Open the [SSR client guide](https://docs-git-docs-ssr-client-feedback-supabase.vercel.app/docs/guides/auth/server-side/creating-a-client) on the deploy preview. The Next.js panel carries the version note, the refresh warning, and the response guidance. 2. Select the refresh token reuse detection link. It resolves to the sessions guide. 3. Open the [Next.js Auth prompt](https://docs-git-docs-ssr-client-feedback-supabase.vercel.app/docs/guides/ai-tools/ai-prompts/nextjs-supabase-auth). The proxy section names the file and says it is `proxy.ts` on Next.js 16 and later. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Documentation - Clarified that `getClaims` refreshes sessions when access tokens are near expiration, helping server-rendered sessions remain active. - Expanded Next.js SSR guidance for session-refresh setup, including file placement and version-specific naming. - Added warnings about refresh-token reuse and session revocation after repeated refreshes outside the reuse window. - Added guidance for preserving authentication cookies and cache-related headers when returning updated responses. - Clarified that refreshed tokens should be passed to Server Components to keep sessions active. - Clarified the required session-refresh handler export and example filename. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1020 lines
32 KiB
Plaintext
1020 lines
32 KiB
Plaintext
---
|
|
title: 'Creating a Supabase client for SSR'
|
|
subtitle: 'Configure your Supabase client to use cookies'
|
|
---
|
|
|
|
Learn how to configure your Supabase client to use cookies. Your app can then render on the server with the user already signed in.
|
|
|
|
Server-Side Rendering (SSR) with Supabase requires cookie-based session storage. The `@supabase/ssr` package handles this for JavaScript and TypeScript applications.
|
|
|
|
Use this guide to:
|
|
|
|
1. [Install the packages](#install).
|
|
2. [Set environment variables](#set-environment-variables).
|
|
3. [Create a client](#create-a-client) for your framework.
|
|
|
|
Refer to these reference sections to make better decisions about verifying users and caching responses:
|
|
|
|
- [Choosing an auth method](#choosing-an-auth-method), before you write code that checks who the user is.
|
|
- [Caching considerations](#caching-considerations), if you deploy behind a CDN or use ISR.
|
|
|
|
## Install
|
|
|
|
Install the `@supabase/supabase-js` and `@supabase/ssr` helper packages:
|
|
|
|
<Tabs size="small" type="underlined" queryGroup="package-manager" defaultActiveId="npm">
|
|
|
|
<TabPanel id="npm" label="npm">
|
|
|
|
```bash
|
|
npm install @supabase/supabase-js @supabase/ssr
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="yarn" label="yarn">
|
|
|
|
```bash
|
|
yarn add @supabase/supabase-js @supabase/ssr
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="pnpm" label="pnpm">
|
|
|
|
```bash
|
|
pnpm add @supabase/supabase-js @supabase/ssr
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
</Tabs>
|
|
|
|
## Set environment variables
|
|
|
|
Create a `.env.local` file in the project root directory. In the file, set the project's Supabase URL and Key:
|
|
|
|
<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} />
|
|
|
|
<Tabs scrollable size="small" type="underlined" defaultActiveId="nextjs" queryGroup="framework">
|
|
|
|
<TabPanel id="nextjs" label="Next.js">
|
|
|
|
```bash .env.local
|
|
NEXT_PUBLIC_SUPABASE_URL=supabase_project_url
|
|
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="sveltekit" label="SvelteKit">
|
|
|
|
```bash .env.local
|
|
PUBLIC_SUPABASE_URL=supabase_project_url
|
|
PUBLIC_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="astro" label="Astro">
|
|
|
|
```bash .env
|
|
PUBLIC_SUPABASE_URL=supabase_project_url
|
|
PUBLIC_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="remix" label="Remix">
|
|
|
|
```bash .env
|
|
SUPABASE_URL=supabase_project_url
|
|
SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="nuxt" label="Nuxt">
|
|
|
|
```bash .env
|
|
NUXT_PUBLIC_SUPABASE_URL=supabase_project_url
|
|
NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
In `nuxt.config.ts`, map these public env vars into runtime config keys used by the examples below:
|
|
|
|
```ts nuxt.config.ts
|
|
export default defineNuxtConfig({
|
|
runtimeConfig: {
|
|
public: {
|
|
// These defaults will be overridden by NUXT_PUBLIC_SUPABASE_URL and
|
|
// NUXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY environment variables at runtime.
|
|
supabaseUrl: '',
|
|
supabasePublishableKey: '',
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="react-router" label="React Router">
|
|
|
|
```bash .env
|
|
SUPABASE_URL=supabase_project_url
|
|
SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="express" label="Express">
|
|
|
|
```bash .env
|
|
SUPABASE_URL=supabase_project_url
|
|
SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
Install [dotenv](https://www.npmjs.com/package/dotenv):
|
|
|
|
<Tabs size="small" type="underlined" queryGroup="package-manager" defaultActiveId="npm">
|
|
|
|
<TabPanel id="npm" label="npm">
|
|
|
|
```bash
|
|
npm install dotenv
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="yarn" label="yarn">
|
|
|
|
```bash
|
|
yarn add dotenv
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="pnpm" label="pnpm">
|
|
|
|
```bash
|
|
pnpm add dotenv
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
</Tabs>
|
|
|
|
Then load the file before you read any variable from it. Put this on the first line of your entry point, above every other import:
|
|
|
|
```js app.js
|
|
require('dotenv').config()
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="hono" label="Hono">
|
|
|
|
```bash .env
|
|
SUPABASE_URL=supabase_project_url
|
|
SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="tanstack" label="TanStack Start">
|
|
|
|
```bash .env.local
|
|
VITE_SUPABASE_URL=supabase_project_url
|
|
VITE_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
## Create a client
|
|
|
|
You need setup code to configure a Supabase client to use cookies. Once you have the utility code, you can use the `createClient` utility functions to get a properly configured Supabase client.
|
|
|
|
Use the browser client in code that runs on the browser, and the server client in code that runs on the server.
|
|
|
|
Before you write code that checks who the user is, see [Choosing an auth method](#choosing-an-auth-method).
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="nextjs"
|
|
queryGroup="framework"
|
|
>
|
|
<TabPanel id="nextjs" label="Next.js">
|
|
|
|
### Write utility functions to create Supabase clients [#nextjs-utility-functions]
|
|
|
|
To access Supabase from a Next.js app, you need 2 types of Supabase clients:
|
|
|
|
1. **Client Component client** - To access Supabase from Client Components, which run in the browser.
|
|
2. **Server Component client** - To access Supabase from Server Components, Server Actions, and Route Handlers, which run only on the server.
|
|
|
|
Since Next.js Server Components can't write cookies, you need a [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them.
|
|
|
|
<Admonition type="note">
|
|
|
|
On Next.js 15 and earlier, a `proxy.ts` file is never called, so sessions never refresh and users get signed out. Next.js renamed this file in version 16. Before that, it's `middleware.ts` and the function is `export async function middleware`. The Supabase code inside it is the same either way.
|
|
|
|
</Admonition>
|
|
|
|
The Proxy is responsible for:
|
|
|
|
1. Refreshing the Auth token by calling `supabase.auth.getClaims()`.
|
|
2. Passing the refreshed Auth token to Server Components, so they don't attempt to refresh the same token themselves. It is what keeps users signed in. This is accomplished with `request.cookies.set`.
|
|
3. Passing the refreshed Auth token to the browser, so it replaces the old token. This is accomplished with `response.cookies.set`.
|
|
|
|
<Accordion>
|
|
|
|
<AccordionItem
|
|
header="What does the `cookies` object do?"
|
|
id="utility-cookies"
|
|
>
|
|
|
|
The cookies object lets the Supabase client know how to access the cookies, so it can read and write the user session data. To make `@supabase/ssr` framework-agnostic, the cookies methods aren't hard-coded. These utility functions adapt `@supabase/ssr`'s cookie handling for Next.js.
|
|
|
|
`setAll` is called whenever the library needs to write cookies, for example after a token refresh. It receives two arguments: the array of cookies to set, and a `headers` object containing the cache headers `Cache-Control`, `Expires`, and `Pragma`, which must be applied to the HTTP response to prevent CDNs from caching the response and leaking the session to other users. In the Proxy, apply these headers to the response. In Server Components, the headers cannot be set, which is why the `setAll` call is wrapped in a try/catch and the error is ignored. The Proxy handles writing cookies and headers on every request.
|
|
|
|
The cookie is named `sb-<project_ref>-auth-token` by default.
|
|
|
|
</AccordionItem>
|
|
|
|
<AccordionItem
|
|
header="Do I need to create a new client for every route?"
|
|
id="client-deduplication"
|
|
>
|
|
|
|
Yes! Creating a Supabase client is lightweight.
|
|
|
|
- On the server, it basically configures a `fetch` call. You need to reconfigure the fetch call anew for every request to your server, because you need the cookies from the request.
|
|
- On the client, `createBrowserClient` already uses a singleton pattern, so you only ever create one instance, no matter how many times you call your `createClient` function.
|
|
|
|
</AccordionItem>
|
|
|
|
<AccordionItem
|
|
header="Why does refreshing in two places sign users out?"
|
|
id="double-refresh"
|
|
>
|
|
|
|
A refresh token can generally be used only once, with two exceptions. Supabase allows a short window in which the same token can be presented again, which covers the normal SSR round trip. It also returns the active token when the parent of the active token is presented, which covers a client that never received the previous response. A reuse attempt that matches neither exception revokes the whole session.
|
|
|
|
This is hard to trace, because it looks like users being signed out at random rather than an error in your code.
|
|
|
|
See [refresh token reuse detection](/docs/guides/auth/sessions#what-is-refresh-token-reuse-detection-and-what-does-it-protect-from).
|
|
|
|
</AccordionItem>
|
|
|
|
</Accordion>
|
|
|
|
Create a `lib/supabase` folder at the root of your project, or inside the `./src` folder if you are using one, with a file for each type of client. Then copy the lib utility functions for each client type.
|
|
|
|
<div className="mt-12">
|
|
<$CodeTabs>
|
|
<$CodeSample
|
|
path="/auth/nextjs/lib/supabase/client.ts"
|
|
meta="name=lib/supabase/client.ts"
|
|
language="typescript"
|
|
/>
|
|
<$CodeSample
|
|
path="/auth/nextjs/lib/supabase/server.ts"
|
|
meta="name=lib/supabase/server.ts"
|
|
language="typescript"
|
|
/>
|
|
</$CodeTabs>
|
|
</div>
|
|
|
|
### Hook up proxy
|
|
|
|
The code adds a [matcher](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#matcher) so the Proxy doesn't run on routes that don't access Supabase.
|
|
|
|
Return the `supabaseResponse` object that `setAll` last built. An earlier response doesn't carry the refreshed cookies, so the user is signed out on the next request.
|
|
|
|
When you need to return a different response, copy the cookies and the cache headers onto it first:
|
|
|
|
```ts
|
|
const myNewResponse = NextResponse.next({ request })
|
|
myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
|
for (const header of ['cache-control', 'expires', 'pragma']) {
|
|
const value = supabaseResponse.headers.get(header)
|
|
if (value) myNewResponse.headers.set(header, value)
|
|
}
|
|
return myNewResponse
|
|
```
|
|
|
|
<Admonition type="danger">
|
|
|
|
Anyone can forge the session cookie, so trusting it without verification lets an attacker render another user's page. Always use `supabase.auth.getClaims()` to protect pages and user data.
|
|
|
|
_Never_ trust `supabase.auth.getSession()` inside server code such as Proxy. It reads the session out of the cookie without revalidating it.
|
|
|
|
`getClaims()` verifies the token's signature on every call. On projects with asymmetric signing keys, the default for new projects, it verifies locally against a cached copy of the project's public keys. On projects still using a symmetric secret, it calls the Auth server instead. Either way the claims come from a token the server has verified rather than from whatever the cookie says.
|
|
|
|
</Admonition>
|
|
|
|
<div className="mt-12">
|
|
<$CodeTabs>
|
|
<$CodeSample path="/auth/nextjs/proxy.ts" meta="name=proxy.ts" language="typescript" />
|
|
<$CodeSample
|
|
path="/auth/nextjs/lib/supabase/proxy.ts"
|
|
meta="name=lib/supabase/proxy.ts"
|
|
language="typescript"
|
|
/>
|
|
</$CodeTabs>
|
|
</div>
|
|
|
|
### Congratulations [#nextjs-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Called Supabase from a Server Action.
|
|
- Called Supabase from a Server Component.
|
|
- Set up a Supabase client utility to call Supabase from a Client Component. You can use this if you need to call Supabase from a Client Component, for example to set up a realtime subscription.
|
|
- Set up Proxy to automatically refresh the Supabase Auth session.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
<TabPanel id="sveltekit" label="SvelteKit">
|
|
|
|
### Set up server-side hooks
|
|
|
|
Set up server-side hooks in `src/hooks.server.ts`. The hooks:
|
|
|
|
- Create a request-specific Supabase client, using the user credentials from the request cookie. This client is used for server-only code.
|
|
- Check user authentication.
|
|
- Guard protected pages.
|
|
|
|
<$CodeSample
|
|
path="/auth/sveltekit/src/hooks.server.ts"
|
|
meta="name=src/hooks.server.ts"
|
|
language="typescript"
|
|
/>
|
|
|
|
To prevent TypeScript errors, add type definitions for the new event.locals properties.
|
|
|
|
<$CodeSample
|
|
path="/auth/sveltekit/src/app.d.ts"
|
|
meta="name=src/app.d.ts"
|
|
language="typescript"
|
|
/>
|
|
|
|
### Create a Supabase client in your root layout
|
|
|
|
Create a Supabase client in your root `+layout.ts`. This client can be used to access Supabase from the client or the server. In order to get access to the Auth token on the server, use a `+layout.server.ts` file to pass in the session from event.locals.
|
|
|
|
Page components can access the Supabase client from the `data` object using the `load` function.
|
|
|
|
<$CodeTabs>
|
|
<$CodeSample
|
|
path="/auth/sveltekit/src/routes/+layout.ts"
|
|
meta="name=src/routes/+layout.ts"
|
|
language="typescript"
|
|
/>
|
|
|
|
<$CodeSample
|
|
path="/auth/sveltekit/src/routes/+layout.server.ts"
|
|
meta="name=src/routes/+layout.server.ts"
|
|
language="typescript"
|
|
/>
|
|
</$CodeTabs>
|
|
|
|
### Congratulations [#sveltekit-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Set up server-side hooks to create a request-specific Supabase client and guard protected pages.
|
|
- Created a Supabase client in your root layout to use on both the client and server.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
<TabPanel id="astro" label="Astro">
|
|
|
|
### Configure Astro for SSR
|
|
|
|
Astro apps are static by default, so requests for data happen at build time rather than when a user requests a page. At build time there is no user, session, or cookie. Configure Astro for SSR if you want data fetched per request.
|
|
|
|
```js astro.config.mjs
|
|
import { defineConfig } from 'astro/config'
|
|
|
|
export default defineConfig({
|
|
output: 'server',
|
|
})
|
|
```
|
|
|
|
### Create the Supabase clients [#astro-create-clients]
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="astro-server"
|
|
queryGroup="environment"
|
|
>
|
|
<TabPanel id="astro-server" label="Server">
|
|
|
|
```ts index.astro
|
|
---
|
|
import { createServerClient, parseCookieHeader } from "@supabase/ssr";
|
|
|
|
const supabase = createServerClient(
|
|
import.meta.env.PUBLIC_SUPABASE_URL,
|
|
import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(Astro.request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, headers) {
|
|
cookiesToSet.forEach(({ name, value }) =>
|
|
Astro.cookies.set(name, value))
|
|
Object.entries(headers).forEach(([key, value]) =>
|
|
Astro.response.headers.set(key, value)
|
|
)
|
|
},
|
|
},
|
|
}
|
|
);
|
|
---
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="astro-browser" label="Browser">
|
|
|
|
```html index.astro
|
|
<script>
|
|
import { createBrowserClient } from "@supabase/ssr";
|
|
|
|
const supabase = createBrowserClient(
|
|
import.meta.env.PUBLIC_SUPABASE_URL,
|
|
import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
);
|
|
</script>
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="astro-server-endpoint" label="Server Endpoint">
|
|
|
|
```ts route.ts
|
|
import { createServerClient, parseCookieHeader } from '@supabase/ssr'
|
|
import type { APIContext } from 'astro'
|
|
|
|
export async function GET(context: APIContext) {
|
|
const responseHeaders = new Headers()
|
|
|
|
const supabase = createServerClient(
|
|
import.meta.env.PUBLIC_SUPABASE_URL,
|
|
import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(context.request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, headers) {
|
|
cookiesToSet.forEach(({ name, value }) => context.cookies.set(name, value))
|
|
Object.entries(headers).forEach(([key, value]) => responseHeaders.set(key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// Build your response here, and pass `responseHeaders` to it. Without them a
|
|
// shared cache can store this response along with its Set-Cookie header.
|
|
return new Response(null, { headers: responseHeaders })
|
|
}
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="astro-middleware" label="Middleware">
|
|
|
|
```ts middleware.ts
|
|
import { createServerClient, parseCookieHeader } from '@supabase/ssr'
|
|
import { defineMiddleware } from 'astro:middleware'
|
|
|
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
const responseHeaders = new Headers()
|
|
|
|
const supabase = createServerClient(
|
|
import.meta.env.PUBLIC_SUPABASE_URL,
|
|
import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(context.request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, headers) {
|
|
cookiesToSet.forEach(({ name, value }) => context.cookies.set(name, value))
|
|
Object.entries(headers).forEach(([key, value]) => responseHeaders.set(key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
const response = await next()
|
|
responseHeaders.forEach((value, key) => response.headers.set(key, value))
|
|
return response
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
### Congratulations [#astro-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Created a server client for code that runs on the server, and a browser client for code that runs in the browser.
|
|
- Read and wrote the session cookie from a server endpoint and from middleware.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
<TabPanel id="remix" label="Remix">
|
|
|
|
### Create the Supabase clients [#remix-create-clients]
|
|
|
|
With Remix, in a route module such as `_index.tsx`, you can export a `loader`, an `action`, and a default component.
|
|
|
|
Configure Supabase clients as follows:
|
|
|
|
1. **Create a server client in the `loader`.** Use it to load data and manage the user session on the server. Return your Supabase URL and publishable key so the browser can create a client.
|
|
2. **Create a server client in the `action`.** Use it to handle form submissions and other mutations on the server.
|
|
3. **Create a browser client in the default component.** Call `useLoaderData` to read the URL and key, then call `createBrowserClient`.
|
|
|
|
The following example shows all three exports in one route module:
|
|
|
|
```ts _index.tsx
|
|
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node'
|
|
import { useLoaderData } from '@remix-run/react'
|
|
import {
|
|
createBrowserClient,
|
|
createServerClient,
|
|
parseCookieHeader,
|
|
serializeCookieHeader,
|
|
} from '@supabase/ssr'
|
|
|
|
// Server: load data and manage the user's session.
|
|
export async function loader({ request }: LoaderFunctionArgs) {
|
|
const responseHeaders = new Headers()
|
|
|
|
const supabase = createServerClient(
|
|
process.env.SUPABASE_URL!,
|
|
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, cacheHeaders) {
|
|
cookiesToSet.forEach(({ name, value, options }) =>
|
|
responseHeaders.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
|
)
|
|
Object.entries(cacheHeaders).forEach(([key, value]) => responseHeaders.set(key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// Use `supabase` here for server-side work, e.g. await supabase.auth.getClaims()
|
|
|
|
// Return the environment variables so the browser can create its own client.
|
|
return json(
|
|
{
|
|
env: {
|
|
SUPABASE_URL: process.env.SUPABASE_URL!,
|
|
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
},
|
|
},
|
|
{ headers: responseHeaders }
|
|
)
|
|
}
|
|
|
|
// Server: handle form submissions and mutations.
|
|
export async function action({ request }: ActionFunctionArgs) {
|
|
const responseHeaders = new Headers()
|
|
|
|
const supabase = createServerClient(
|
|
process.env.SUPABASE_URL!,
|
|
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, cacheHeaders) {
|
|
cookiesToSet.forEach(({ name, value, options }) =>
|
|
responseHeaders.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
|
)
|
|
Object.entries(cacheHeaders).forEach(([key, value]) => responseHeaders.set(key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
return json(null, { headers: responseHeaders })
|
|
}
|
|
|
|
// Browser: create a client using the env vars returned by the loader.
|
|
export default function Index() {
|
|
const { env } = useLoaderData<typeof loader>()
|
|
|
|
const supabase = createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY)
|
|
|
|
return <div>...</div>
|
|
}
|
|
```
|
|
|
|
### Congratulations [#remix-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Created a server client in the `loader` to load data and manage the session.
|
|
- Created a server client in the `action` to handle form submissions and mutations.
|
|
- Created a browser client in the default component, using the values the `loader` returned.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="nuxt" label="Nuxt">
|
|
|
|
### Create the Supabase clients [#nuxt-create-clients]
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="nuxt-server"
|
|
queryGroup="environment"
|
|
>
|
|
<TabPanel id="nuxt-server" label="Server route">
|
|
|
|
```ts server/api/hello.ts
|
|
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'
|
|
import { appendHeader, defineEventHandler, getHeader, setHeader } from 'h3'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const config = useRuntimeConfig()
|
|
|
|
const supabase = createServerClient(
|
|
config.public.supabaseUrl,
|
|
config.public.supabasePublishableKey,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(getHeader(event, 'Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, cacheHeaders) {
|
|
cookiesToSet.forEach(({ name, value, options }) => {
|
|
appendHeader(event, 'Set-Cookie', serializeCookieHeader(name, value, options))
|
|
})
|
|
Object.entries(cacheHeaders).forEach(([key, value]) => setHeader(event, key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
await supabase.auth.getClaims()
|
|
|
|
return { ok: true }
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="nuxt-browser" label="Browser plugin">
|
|
|
|
```ts plugins/supabase.client.ts
|
|
import { createBrowserClient } from '@supabase/ssr'
|
|
|
|
export default defineNuxtPlugin(() => {
|
|
const config = useRuntimeConfig()
|
|
|
|
const supabase = createBrowserClient(
|
|
config.public.supabaseUrl,
|
|
config.public.supabasePublishableKey
|
|
)
|
|
|
|
return {
|
|
provide: {
|
|
supabase,
|
|
},
|
|
}
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
### Congratulations [#nuxt-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Created a server client in a server route for code that runs on the server.
|
|
- Created a browser client in a plugin for code that runs in the browser.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="react-router" label="React Router">
|
|
|
|
### Create the Supabase clients [#react-router-create-clients]
|
|
|
|
In React Router, a route module such as `_index.tsx` can export a `loader`, an `action`, and a default component. Create a server client inside the `loader` and `action`, and a browser client inside the component, passing the env vars through the `loader`.
|
|
|
|
```ts _index.tsx
|
|
import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from 'react-router'
|
|
import { useLoaderData } from 'react-router'
|
|
import {
|
|
createBrowserClient,
|
|
createServerClient,
|
|
parseCookieHeader,
|
|
serializeCookieHeader,
|
|
} from '@supabase/ssr'
|
|
|
|
// Server: load data and manage the user's session.
|
|
export async function loader({ request }: LoaderFunctionArgs) {
|
|
const responseHeaders = new Headers()
|
|
|
|
const supabase = createServerClient(
|
|
process.env.SUPABASE_URL!,
|
|
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, cacheHeaders) {
|
|
cookiesToSet.forEach(({ name, value, options }) =>
|
|
responseHeaders.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
|
)
|
|
Object.entries(cacheHeaders).forEach(([key, value]) => responseHeaders.set(key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// Use `supabase` here for server-side work, e.g. await supabase.auth.getClaims()
|
|
|
|
// Return the env vars so the browser can create its own client.
|
|
return data(
|
|
{
|
|
env: {
|
|
SUPABASE_URL: process.env.SUPABASE_URL!,
|
|
SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
},
|
|
},
|
|
{ headers: responseHeaders }
|
|
)
|
|
}
|
|
|
|
// Server: handle form submissions and mutations.
|
|
export async function action({ request }: ActionFunctionArgs) {
|
|
const responseHeaders = new Headers()
|
|
|
|
const supabase = createServerClient(
|
|
process.env.SUPABASE_URL!,
|
|
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
{
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(request.headers.get('Cookie') ?? '')
|
|
},
|
|
setAll(cookiesToSet, cacheHeaders) {
|
|
cookiesToSet.forEach(({ name, value, options }) =>
|
|
responseHeaders.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
|
)
|
|
Object.entries(cacheHeaders).forEach(([key, value]) => responseHeaders.set(key, value))
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
return data(null, { headers: responseHeaders })
|
|
}
|
|
|
|
// Browser: create a client using the env vars returned by the loader.
|
|
export default function Index() {
|
|
const { env } = useLoaderData<typeof loader>()
|
|
|
|
const supabase = createBrowserClient(env.SUPABASE_URL, env.SUPABASE_PUBLISHABLE_KEY)
|
|
|
|
return <div>...</div>
|
|
}
|
|
```
|
|
|
|
### Congratulations [#react-router-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Created a server client in the `loader` and the `action`.
|
|
- Created a browser client in the default component, using the values the `loader` returned.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="express" label="Express">
|
|
|
|
### Create the Supabase clients [#express-create-clients]
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="server-client"
|
|
queryGroup="environment"
|
|
>
|
|
<TabPanel id="server-client" label="Server Client">
|
|
|
|
```js lib/supabase.js
|
|
const { createServerClient, parseCookieHeader, serializeCookieHeader } = require('@supabase/ssr')
|
|
|
|
exports.createClient = (context) => {
|
|
return createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_PUBLISHABLE_KEY, {
|
|
cookies: {
|
|
getAll() {
|
|
return parseCookieHeader(context.req.headers.cookie ?? '')
|
|
},
|
|
setAll(cookiesToSet, headers) {
|
|
cookiesToSet.forEach(({ name, value }) =>
|
|
context.res.appendHeader('Set-Cookie', serializeCookieHeader(name, value))
|
|
)
|
|
Object.entries(headers).forEach(([key, value]) => context.res.setHeader(key, value))
|
|
},
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
</TabPanel>
|
|
<TabPanel id="express-route" label="Route">
|
|
|
|
```js app.js
|
|
require("dotenv").config()
|
|
|
|
const express = require("express")
|
|
|
|
const { createClient } = require("./lib/supabase")
|
|
|
|
const app = express()
|
|
|
|
app.post("/hello-world", async function (req, res, next) {
|
|
const { email, emailConfirm } = req.body
|
|
...
|
|
|
|
const supabase = createClient({ req, res })
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
### Congratulations [#express-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Created a request-specific server client.
|
|
- Used that client in a route to make authenticated requests.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="hono" label="Hono">
|
|
|
|
### Create the Supabase clients [#hono-create-clients]
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="server-client"
|
|
queryGroup="environment"
|
|
>
|
|
<TabPanel id="server-client" label="Server Client">
|
|
|
|
Create a Hono middleware that creates a Supabase client.
|
|
|
|
<$CodeSample
|
|
path="/auth/hono/src/middleware/auth.middleware.ts"
|
|
meta="name=src/middleware/auth.middleware.ts"
|
|
language="typescript"
|
|
/>
|
|
|
|
</TabPanel>
|
|
<TabPanel id="hono-route" label="Route">
|
|
|
|
You can now use this middleware in your Hono application to create a server Supabase client that can be used to make authenticated requests.
|
|
|
|
<$CodeSample
|
|
path="/auth/hono/src/index.tsx"
|
|
meta="name=src/index.tsx"
|
|
language="typescript"
|
|
/>
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
### Congratulations [#hono-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Created a Hono middleware that builds a request-specific server client.
|
|
- Used that client in a route to make authenticated requests.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
<TabPanel id="tanstack" label="TanStack Start">
|
|
|
|
### Write utility functions to create Supabase clients [#tanstack-utility-functions]
|
|
|
|
TanStack Start renders matched routes on the server by default, so `beforeLoad` and `loader` run server-side on the initial request. Unlike Next.js, this means you don't need a proxy or middleware layer to keep sessions fresh. The server client reads and writes the session cookie directly on each request.
|
|
|
|
Create a `lib/supabase` folder at the root of your project, or inside the `./src` folder if you are using one, then add a file for each type of client:
|
|
|
|
1. **Create a browser client in `lib/supabase/client.ts`.** Use it to access Supabase from components that run in the browser.
|
|
2. **Create a server client in `lib/supabase/server.ts`.** Use it to access Supabase from loaders, server functions, and other code that runs only on the server.
|
|
|
|
Copy the lib utility functions below into each file:
|
|
|
|
<div className="mt-12">
|
|
<$CodeTabs>
|
|
<$CodeSample
|
|
path="/auth/tanstack/lib/supabase/client.ts"
|
|
meta="name=lib/supabase/client.ts"
|
|
language="typescript"
|
|
/>
|
|
<$CodeSample
|
|
path="/auth/tanstack/lib/supabase/server.ts"
|
|
meta="name=lib/supabase/server.ts"
|
|
language="typescript"
|
|
/>
|
|
</$CodeTabs>
|
|
</div>
|
|
|
|
### Protecting routes
|
|
|
|
TanStack Start has no global middleware layer, so protect each route explicitly.
|
|
|
|
To protect your routes:
|
|
|
|
1. Write a server function, `fetchClaims`, that calls `supabase.auth.getClaims()` and returns the claims, or `null` if the session isn't valid.
|
|
1. Call `fetchClaims` from a layout route's `beforeLoad` hook, such as `_protected.tsx`, before any nested route renders. Redirect to `/login` when it returns `null`.
|
|
|
|
<Admonition type="danger">
|
|
|
|
Skipping the check inside the server function exposes private data to unauthenticated users. `beforeLoad` runs on the server for the initial request and on the client for later navigation, but either way it only gates the route's render. It doesn't stop the server function from being called directly. Because there's no proxy re-checking every request, the server function is the only checkpoint that always runs, so it must call `supabase.auth.getClaims()` to authorize the request itself.
|
|
|
|
</Admonition>
|
|
|
|
`getClaims()` validates the JWT signature on every call, the same check the Next.js Proxy relies on. Calling it inside the server function gives TanStack Start's per-route check that same guarantee, because the function runs on every request to a protected route.
|
|
|
|
<div className="mt-12">
|
|
<$CodeTabs>
|
|
<$CodeSample
|
|
path="/auth/tanstack/lib/supabase/fetch-claims-server-fn.ts"
|
|
meta="name=lib/supabase/fetch-claims-server-fn.ts"
|
|
language="typescript"
|
|
/>
|
|
<$CodeSample
|
|
path="/auth/tanstack/routes/_protected.tsx"
|
|
meta="name=routes/_protected.tsx"
|
|
language="typescript"
|
|
/>
|
|
</$CodeTabs>
|
|
</div>
|
|
|
|
Any other server function that returns or mutates private data needs this same check. Don't rely on a route being nested under `_protected` alone.
|
|
|
|
### Congratulations [#tanstack-congratulations]
|
|
|
|
To recap, you've:
|
|
|
|
- Set up a Supabase client utility to call Supabase from a browser component. You can use this if you need to call Supabase from the browser, for example to set up a realtime subscription.
|
|
- Set up a server client utility to call Supabase from loaders and server functions.
|
|
- Protected a route with `beforeLoad`, backed by a server function that authorizes the request itself.
|
|
|
|
You can now use any Supabase feature from your client or server code.
|
|
|
|
</TabPanel>
|
|
</Tabs>
|
|
|
|
## Choosing an auth method
|
|
|
|
<$Partial path="auth_methods.mdx" />
|
|
|
|
## Caching considerations
|
|
|
|
If your app uses ISR (Incremental Static Regeneration) or is deployed behind a CDN, caching of HTTP responses can cause users to receive another user's session. When a session is refreshed, the new token is written to the response via `Set-Cookie`. If that response is cached and served to a different user, that user will be signed in as the wrong person.
|
|
|
|
See the [advanced Auth server-side rendering guide](/docs/guides/auth/server-side/advanced-guide#can-i-use-server-side-rendering-with-a-cdn-or-cache) for details and framework-specific examples.
|
|
|
|
## Next steps
|
|
|
|
- Implement [Authentication using Email and Password](/docs/guides/auth/passwords)
|
|
- Implement [Authentication using OAuth](/docs/guides/auth/social-login)
|
|
- [Learn more about SSR](/docs/guides/auth/server-side/advanced-guide)
|