docs(auth): tighten the voice in the SSR client guide (#50286)

## Problem

The SSR client guide, like all guides, have drifted from our style rules
and writing best practices.

This PR is to do an inline edit without re-arranging any sections.

## Solution

- Open with what the guide does, then the SSR context.
- Delete the `{/* TODO: Can this be consolidated? */}` comment.
- Remove the three em dashes and the parenthetical asides in prose.
- Rewrite the Next.js danger callout to lead with the consequence:
anyone can forge the session cookie.
- Give Astro, Remix, Nuxt, React Router, and Express the same bulleted
recap Next.js, SvelteKit, and TanStack already had.

## Manual testing

1. Open the [SSR client
guide](https://docs-git-docs-ssr-client-style-supabase.vercel.app/docs/guides/auth/server-side/creating-a-client)
on the deploy preview. The first sentence says what the guide does.
2. Select each framework tab. Every panel ends with a bulleted recap.

Part of DOCS-1313.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Updated server-side authentication guidance across supported
frameworks.
- Clarified cookie-based session storage, SSR package usage, and
cache-header handling.
- Added guidance on protecting against forged cookies and verifying
sessions with `getClaims()`.
- Expanded framework setup and authentication flow summaries for Astro,
Remix, Nuxt, React Router, Express, and TanStack Start.
- Clarified TanStack route protection, redirects, and server-side
authorization requirements.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Miranda Limonczenko authored and GitHub committed 2026-09-16 11:41:47 -07:00
1 parent 3e6b40b238
commit cf5bf65361
1 file changed
+47 -22
@@ -3,7 +3,9 @@ title: 'Creating a Supabase client for SSR'
subtitle: 'Configure your Supabase client to use cookies'
---
To use Server-Side Rendering (SSR) with Supabase, you need to configure your Supabase client to use cookies. The `@supabase/ssr` package helps you do this for JavaScript/TypeScript applications.
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.
## Install
@@ -172,7 +174,6 @@ VITE_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
## Create a client
{/* TODO: Can this be consolidated? */}
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.
@@ -214,7 +215,7 @@ The Proxy is responsible for:
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 cache headers (`Cache-Control`, `Expires`, `Pragma`) that 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.
`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.
@@ -257,9 +258,7 @@ The code adds a [matcher](https://nextjs.org/docs/app/api-reference/file-convent
<Admonition type="danger">
Be careful when protecting pages. The server gets the user session from the cookies, which can be spoofed by anyone.
Always use `supabase.auth.getClaims()` to protect pages and user data.
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 isn't guaranteed to revalidate the Auth token.
@@ -282,14 +281,14 @@ It's safe to trust `getClaims()` because it validates the JWT signature against
## Congratulations
You're done! To recap, you've successfully:
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 features from your client or server code!
You can now use any Supabase feature from your client or server code.
</TabPanel>
<TabPanel id="sveltekit" label="SvelteKit">
@@ -340,17 +339,17 @@ language="typescript"
## Congratulations
You're done! To recap, you've successfully:
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 features from your client or server code!
You can now use any Supabase feature from your client or server code.
</TabPanel>
<TabPanel id="astro" label="Astro">
By default, Astro apps are static. This means the requests for data happen at build time, rather than when the user requests a page. At build time, there is no user, session or cookies. Therefore, we need to configure Astro for Server-side Rendering (SSR) if you want data to be fetched dynamically per request.
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'
@@ -471,7 +470,12 @@ export const onRequest = defineMiddleware(async (context, next) => {
## Congratulations
You can now use any Supabase features from your client or server code!
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">
@@ -569,7 +573,13 @@ export default function Index() {
## Congratulations
You can now use any Supabase features from your client or server code!
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>
@@ -642,13 +652,18 @@ export default defineNuxtPlugin(() => {
## Congratulations
You can now use any Supabase features from your client or server code!
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">
In React Router, a route module (`_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`.
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'
@@ -733,7 +748,12 @@ export default function Index() {
## Congratulations
You can now use any Supabase features from your client or server code!
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>
@@ -792,7 +812,12 @@ app.post("/hello-world", async function (req, res, next) {
## Congratulations
You can now use any Supabase features from your client or server code!
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>
@@ -836,7 +861,7 @@ language="typescript"
### Write utility functions to create Supabase clients
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.
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:
@@ -869,11 +894,11 @@ 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 — for example, `_protected.tsx` — before any nested route renders, and redirect to `/login` when it returns `null`.
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.
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>
@@ -898,13 +923,13 @@ Any other server function that returns or mutates private data needs this same c
## Congratulations
You're done! To recap, you've successfully:
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 features from your client or server code!
You can now use any Supabase feature from your client or server code.
</TabPanel>
</Tabs>