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:
- Access token in the form of a JWT.
- 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 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. 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#
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#
No session on the server side with Next.js 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?#
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?#
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?#
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?#
Make sure you understand the behavior of the property in different situations 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?#
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)#
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#
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.
To prevent this, set Cache-Control: private, no-store on responses from any route that handles authentication, typically your middleware. Most CDNs respect this header and will not cache the response.
Next.js middleware#
1const response = NextResponse.next()2// ... supabase client setup and getUser() call3response.headers.set('Cache-Control', 'private, no-store')4return responseNuxt 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-storeto take effect as intended. - Use
Cache-Control: no-cache="Set-Cookie"to instruct CloudFront not to cache theSet-Cookieheader 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
CachingDisabledpolicy 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'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?#
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.