
# 

Advanced guide

## 

Details about SSR Auth flows and implementation for advanced users.

* * *

When a user authenticates with Supabase Auth, two pieces of information are issued by the server:

1.  **Access token** in the form of a JWT.
2.  **Refresh token** which is a randomly generated string.

The default behavior if you're not using SSR is to store this information in local storage. Local storage isn't accessible by the server, so for SSR, the tokens instead need to be stored in a secure cookie. The cookie can then be passed back and forth between your app code in the client and your app code in the server.

If you're not using SSR, you might also be using the [implicit flow](../../sessions/implicit-flow/index.md) to get the access and refresh tokens. The server can't access the tokens in this flow, so for SSR, you should change to the [PKCE flow](../../sessions/pkce-flow/index.md). You can change the flow type when initiating your Supabase client if your client library provides this option.

In the `@supabase/ssr` package, Supabase clients are initiated to use the PKCE flow by default. They are also automatically configured to handle the saving and retrieval of session information in cookies.

## How it works[#](#how-it-works)

In the PKCE flow, a redirect is made to your app, with an Auth Code contained in the URL. When you exchange this code using `exchangeCodeForSession`, you receive the session information, which contains the access and refresh tokens.

To maintain the session, these tokens must be stored in a storage medium securely shared between client and server, which is traditionally cookies. Whenever the session is refreshed, the auth and refresh tokens in the shared storage medium must be updated. Supabase client libraries provide a customizable `storage` option when a client is initiated, allowing you to change where tokens are stored.

## Frequently asked questions[#](#frequently-asked-questions)

### No session on the server side with Next.js route prefetching?[#](#no-session-on-the-server-side-with-nextjs-route-prefetching)

When you use route prefetching in Next.js using `<Link href="/...">` components or the `Router.push()` APIs can send server-side requests before the browser processes the access and refresh tokens. This means that those requests may not have any cookies set and your server code will render unauthenticated content.

To improve experience for your users, we recommend redirecting users to one specific page after sign-in that does not include any route prefetching from Next.js. Once the Supabase client library running in the browser has obtained the access and refresh tokens from the URL fragment, you can send users to any pages that use prefetching.

### How do I make the cookies `HttpOnly`?[#](#how-do-i-make-the-cookies-httponly-)

This is not necessary. Both the access token and refresh token are designed to be passed around to different components in your application. The browser-based side of your application needs access to the refresh token to properly maintain a browser session anyway.

### My server is getting invalid refresh token errors. What's going on?[#](#my-server-is-getting-invalid-refresh-token-errors-whats-going-on)

It is likely that the refresh token sent from the browser to your server is stale. Make sure the `onAuthStateChange` listener callback is free of bugs and is registered relatively early in your application's lifetime

When you receive this error on the server-side, try to defer rendering to the browser where the client library can access an up-to-date refresh token and present the user with a better experience.

### Should I set a shorter `Max-Age` parameter on the cookies?[#](#should-i-set-a-shorter-max-age-parameter-on-the-cookies)

The `Max-Age` or `Expires` cookie parameters only control whether the browser sends the value to the server. Since a refresh token represents the long-lived authentication session of the user on that browser, setting a short `Max-Age` or `Expires` parameter on the cookies only results in a degraded user experience.

The only way to ensure that a user has logged out or their session has ended is to get the user's details with `getUser()`. The `getClaims()` method only checks local JWT validation (signature and expiration), but it doesn't verify with the auth server whether the session is still valid or if the user has logged out server-side.

### What should I use for the `SameSite` property?[#](#what-should-i-use-for-the-samesite-property)

Make sure you [understand the behavior of the property in different situations](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) as some properties can degrade the user experience.

A good default is to use `Lax` which sends cookies when users are navigating to your site. Cookies typically require the `Secure` attribute, which only sends them over HTTPS. However, this can be a problem when developing on `localhost`.

### Can I use server-side rendering with a CDN or cache?[#](#can-i-use-server-side-rendering-with-a-cdn-or-cache)

Yes, but there are two specific scenarios that can cause users to receive another user's session. Both are related to caching of HTTP responses that contain `Set-Cookie` headers.

#### ISR (incremental static regeneration)[#](#isr-incremental-static-regeneration)

If you use ISR on pages that trigger a Supabase session refresh, the cached response will include the `Set-Cookie` header containing the refreshed JWT. When that cached response is served to a subsequent user, their browser stores the token and they are signed in as the wrong person.

Do not enable ISR on any route where authentication is handled or where a session refresh can occur. In Nuxt, avoid setting `isr` on authenticated routes. In Next.js, use `export const dynamic = 'force-dynamic'` on pages that require authentication.

#### CDN and reverse proxy caching[#](#cdn-and-reverse-proxy-caching)

When `@supabase/ssr` refreshes a session token server-side, it writes the updated JWT to the HTTP response via a `Set-Cookie` header. If your CDN (e.g. Vercel Edge, Cloudflare) caches that response and serves it to a different user, that user's browser will store the cached token and be signed in as the wrong person.

As of `@supabase/ssr` v0.10.0, the library automatically passes the necessary cache headers (`Cache-Control`, `Expires`, `Pragma`) to your `setAll` callback as a second argument whenever a token refresh occurs. If your `setAll` implementation applies those headers to the response (as shown in the examples in [Creating a Supabase client for SSR](../creating-a-client/index.md)), no additional manual configuration is needed for most CDNs.

If you are on an older version or need to set headers manually, add `Cache-Control: private, no-store` to responses from any route that handles authentication:

#### Next.js middleware[#](#nextjs-middleware)

```
1const response = NextResponse.next()2// ... supabase client setup and getUser() call3response.headers.set('Cache-Control', 'private, no-store')4return response
```

#### Nuxt server middleware[#](#nuxt-server-middleware)

```
1// ... supabase client setup and getUser() call2setHeader(event, 'Cache-Control', 'private, no-store')
```

**CloudFront**

CloudFront's behavior depends on its cache policy configuration and is not solely controlled by the `Cache-Control` response header. Even with `Cache-Control: private, no-store`, CloudFront can still cache the response and the `Set-Cookie` header if its cache policy has a Minimum TTL greater than 0, or if cookies and the `Set-Cookie` header are not forwarded to the origin.

To protect against session leakage on CloudFront, use one or more of the following steps:

*   **Set Minimum TTL to 0** in your CloudFront cache policy. This allows `Cache-Control: no-store` to take effect as intended.
*   **Use `Cache-Control: no-cache="Set-Cookie"`** to instruct CloudFront not to cache the `Set-Cookie` header specifically, while still allowing other parts of the response to be cached.
*   **Disable caching entirely** for authenticated routes (e.g. your middleware path) by associating a cache policy with TTL set to 0, or by using the managed `CachingDisabled` policy for those behaviors.

On other managed CDN platforms (for example AWS Amplify), cache policies are configured at the platform level and may similarly not fully respect `Cache-Control` headers set in your application. Always verify your CDN's caching behavior for routes that set cookies.

If you need to cache SSR pages for performance, apply caching only to routes that do not write `Set-Cookie` headers, and always include the refresh token cookie value in the cache key for any routes that serve user-specific content.

#### Vercel Fluid compute (in-memory client sharing)[#](#vercel-fluid-compute-in-memory-client-sharing)

Vercel's Fluid compute model can keep server instances warm and reuse them across requests. In some cases this means a Supabase client initialized in module scope — or stored in a shared variable — may be reused across requests from different users, causing one user's session to leak into another user's request.

Always initialize the Supabase client inside the request handler, not at module level. Do not store the client or any user-specific state in a variable that persists between requests.

### Which authentication flows have PKCE support?[#](#which-authentication-flows-have-pkce-support)

At present, PKCE is supported on the Magic Link, OAuth, Sign Up, and Password Recovery routes. These correspond to the `signInWithOtp`, `signInWithOAuth`, `signUp`, and `resetPasswordForEmail` methods on the Supabase client library. When using PKCE with Phone and Email OTPs, there is no behavior change with respect to the implicit flow - an access token will be returned in the body when a request is successful.
