Supabase SSR

repository·main·Indexed 42 days ago

https://github.com/supabase/ssr

A library providing Supabase clients optimized for Server-Side Rendering (SSR) frameworks. It manages user sessions by synchronizing authentication state between the client and server using cookies. This package replaces the deprecated @supabase/auth-helpers-* packages and includes utilities like createBrowserClient and createServerClient to handle session storage, cookie chunking, and Base64-URL encoding.

Tokens
7.2K
Snippets
12
Records
30
Agent score
51%

What's inside Supabase SSR

  1. How cookie encoding works (Base64-URL)

    main

    Since Supabase Auth stores JSON data, but cookies are restricted to US ASCII characters (forbidding characters like ", ,, ;, and whitespace), the library uses a specific encoding strategy for versions > 0.3.0:

    1. The value is prefixed with base64-.
    2. The actual data is encoded using Base64-URL (without whitespace or padding = characters).
    3. If the combined prefix and encoded string exceeds 3180 bytes, the entire string is chunked.

    Decoding logic:

    • If a cookie value starts with base64-, the library decodes the remainder from Base64.
    • If it has a different recognized prefix, it uses that algorithm.
    • If no prefix is found, it attempts to read the value as raw text.
  2. Understand the cookie chunking strategy

    main

    To bypass browser limits on individual cookie sizes (typically ~3180 bytes), this library implements a chunking strategy:

    1. Non-chunked: If the value is $\le$ 3180 bytes, it is stored under the full cookie name key.
    2. Chunked: If the value is $\ge$ 3180 bytes, it is split into chunks of 3180 bytes. The names follow the pattern key.chunk_index (e.g., key.0, key.1), where the index is 0-based.

    Reading logic:

    • The library first checks for the full key.
    • If not found or if chunks exist, it looks for key.0, key.1, etc., and joins them sequentially until a missing index is encountered.
  3. How session data flows in SSR applications

    main

    In Server-Side Rendering (SSR) frameworks, the server needs access to the user's session (access token and refresh token) to render HTML based on the user's state.

    Because JavaScript only runs after the server response is received, the server must handle session management during the initial request. When a user visits an SSR page, the browser sends cookies containing the tokens. If the access token is expired, the server must use the refresh token to obtain a new access token and then send that new token back to the browser using Set-Cookie headers.

  4. How cookie management works in SSR frameworks

    main

    The @supabase/ssr library is designed to handle the different ways SSR frameworks access and modify cookies across four common patterns:

    1. Middleware: Full access to request and response. This is the only place where you can reliably set cookies to ensure session refreshes work correctly.
    2. Routes or APIs: Full access to request and response. Useful for handling forms or privileged tasks.
    3. Server Pages and Components: Read-only access to cookies. You cannot set cookies here. If you attempt to mutate state that requires setting cookies in this pattern, the library will emit a warning to help you identify this code smell.
    4. Client (Browser) Components: Full access to document.cookie for reading and writing.

    Because of these constraints, the createServerClient function requires an interface to bridge these patterns, specifically through getAll and setAll methods.

  5. Understand @supabase/ssr client behavior and limitations

    main

    When using @supabase/ssr, keep the following architectural behaviors in mind:

    Session Storage

    createBrowserClient and createServerClient are designed to store sessions in cookies to ensure the server and browser share the same session state. Because of this, the auth.storage option is ignored and will only trigger a one-time console warning. If you do not require server-side session access, use @supabase-js directly instead.

    Concurrent Request Race Conditions

    Supabase refresh tokens are single-use. If multiple requests (e.g., from multiple browser tabs) arrive simultaneously with the same expired session, the first request will succeed in refreshing the token, but the second will fail because the token has already been consumed. The second request will receive session: null until the browser receives the updated cookie from the first request.

    To mitigate this, use the middleware pattern to refresh the session once per navigation before the page renders.

  6. Managing cookie lifecycle with Max-Age

    main

    When implementing cookie storage for sessions, treat cookies like a key-value database:

    • To Update/Insert: Use the Set-Cookie header with a very high Max-Age value. This ensures the browser persists the session and continues sending it to the server.
    • To Delete: Set the Max-Age option to 0. This is critical for removing stale chunks or clearing old session data. Failing to send Max-Age=0 for stale cookies can result in the library reading outdated or garbled session information.
  7. Use `getAll` and `setAll` instead of `get`, `set`, and `remove`

    main

    Starting in version 0.4.0, the individual cookie access methods get, set, and remove are deprecated. You must use getAll and setAll to ensure session reliability.

    Why this is required: When a session value changes size (e.g., moving from a single cookie to multiple chunks, or vice versa), the library must explicitly clear stale cookies to prevent reading garbled or outdated data. get and set cannot see the full state of all existing chunks, whereas getAll allows the library to inspect the entire request and setAll allows it to issue the necessary Set-Cookie commands to both update new values and delete stale ones (using Max-Age=0).

    Warning: In the next major version, get, set, and remove will no longer be supported.

  8. Install @supabase/ssr

    main

    Install the @supabase/ssr package using your preferred package manager to enable framework-agnostic Supabase client usage in SSR environments.

    This package replaces the deprecated @supabase/auth-helpers-* packages (e.g., @supabase/auth-helpers-nextjs, @supabase/auth-helpers-react, etc.).

    npm i @supabase/ssr
    # or
    pnpm add @supabase/ssr
    # or
    yarn add @supabase/ssr
    # or
    bun add @supabase/ssr
  9. Implement React Router middleware for Supabase

    main

    In React Router, you can use middleware to create a server-side Supabase client, refresh the session once per request, and write updated auth cookies back to the response. This ensures that loaders and actions have access to a valid session.

    1. Create a Context: Define a context to hold the SupabaseClient.
    2. Implement Middleware: Use createServerClient along with parseCookieHeader and serializeCookieHeader to manage cookies within the middleware lifecycle.
    3. Attach Middleware: Apply the middleware to a parent route so child loaders can retrieve the client from the context.
    // app/context.ts
    import { createContext } from "react-router";
    import type { SupabaseClient } from "@supabase/supabase-js";
    
    export const supabaseContext = createContext<SupabaseClient | null>(null);
    // app/middleware/supabase.ts
    import {
      createServerClient,
      parseCookieHeader,
      serializeCookieHeader,
    } from "@supabase/ssr";
    import type { CookieOptions } from "@supabase/ssr";
    import type { MiddlewareFunction } from "react-router";
    import { supabaseContext } from "~/context";
    
    type PendingCookie = {
      name: string;
      value: string;
      options: CookieOptions;
    };
    
    export const supabaseMiddleware: MiddlewareFunction<Response> = async (
      { request, context },
      next,
    ) => {
      const pendingCookies: PendingCookie[] = [];
      const pendingHeaders: Record<string, string> = {};
    
      const supabase = createServerClient(
        process.env.SUPABASE_URL!,
        process.env.SUPABASE_PUBLISHABLE_KEY!,
        {
          cookies: {
            getAll() {
              return parseCookieHeader(request.headers.get("Cookie") ?? "");
            },
            setAll(cookiesToSet, headers) {
              pendingCookies.push(...cookiesToSet);
              Object.assign(pendingHeaders, headers);
            },
          },
        },
      );
    
      // Trigger lazy session init / refresh before any route code runs.
      await supabase.auth.getClaims();
      context.set(supabaseContext, supabase);
    
      const response = await next();
    
      for (const { name, value, options } of pendingCookies) {
        response.headers.append(
          "Set-Cookie",
          serializeCookieHeader(name, value, options),
        );
      }
      for (const [key, value] of Object.entries(pendingHeaders)) {
        response.headers.set(key, value);
      }
    
      return response;
    };
    // app/routes/home.tsx
    import type { Route } from "./+types/home";
    import { supabaseMiddleware } from "~/middleware/supabase";
    import { supabaseContext } from "~/context";
    
    export const middleware: Route.MiddlewareFunction[] = [supabaseMiddleware];
    
    export async function loader({ context }: Route.LoaderArgs) {
      const supabase = context.get(supabaseContext);
      const { data } = await supabase!.auth.getClaims();
      return { claims: data?.claims ?? null };
    }
  10. Mandatory Middleware pattern for session refreshes

    main

    To ensure user sessions are properly refreshed, using the Middleware pattern is mandatory.

    Session refresh typically happens when a user performs a fresh page load (e.g., opening a new tab) and the access token in their cookies has expired. The flow is:

    1. Middleware is invoked.
    2. createServerClient is initialized with getAll.
    3. The client detects an expired token.
    4. The client calls Supabase Auth to refresh the token.
    5. The client calls setAll with the new cookies.
    6. The Set-Cookie headers are returned to the browser.

    If you do not use middleware to call setAll, the refreshed session will not be persisted to the browser, and the user will appear logged out on subsequent requests.

  11. Configure cookie methods for createServerClient

    main

    When using createServerClient, you must provide a cookies object.

    Provide getAll and setAll methods. This is the required pattern for modern SSR frameworks to ensure that authentication state (like refreshed tokens) is correctly persisted back to the user's browser via the response headers.

    Deprecated Pattern

    Providing get, set, and remove is deprecated and will be removed in a future major version. These methods are discouraged because they miss critical edge cases required for reliable SSR authentication.

    You can also provide cookieOptions to customize how cookies are handled, such as specifying a custom name via cookieOptions.name.

  12. Handle PKCE verifier cookie cleanup

    main

    The SDK uses specific cookie keys to manage PKCE (Proof Key for Code Exchange) flows. If you are manually managing or inspecting cookies, be aware of these patterns:

    • Verifier Slots: Keys matching /-flow-[A-Za-z0-9_-]{8,64}-code-verifier$/ are individual slots for in-flight flows.
    • Flow Index: Keys ending with -flows-code-verifier act as an index of pending flow IDs.

    When these specific keys are removed on the server, applyServerStorage is triggered immediately to ensure the browser's cookie state stays in sync with the server's auth state, preventing orphaned verifier cookies.