Cross-subdomain SSO with Supabase SSR cookies
Build cross-subdomain sign-in with an explicit trust boundary, safe cookie attributes, server-side token refresh, and a tested rollback path.
As of 2026-07-29, Supabase's SSR guidance still centers on storing the session in cookies and refreshing tokens on the server. Cross-subdomain SSO is not a matter of copying browser localStorage from one site to another. It begins with an explicit parent-domain trust boundary, followed by a server-managed cookie contract. That contract affects sign-in, sign-out, CSRF, caching, and incident recovery at the same time. Assume the product runs at www.example.com , class.example.com , and app.example.com . A parent-domain cookie is worth considering only when the same organization controls all three hosts and they share the intended Supabase project. If a third party can deploy or serve content from any covered subdomain, expanding an authentication cookie to .example.com expands the attack surface. Prefer a one-time authorization code or a central identity broker in that design instead of sharing the cookie. Implementation steps First, document the trust boundary. List who controls DNS, TLS, deployments, and response headers for every host that may receive the cookie. Add each legitimate callback to the Supabase Auth redirect allowlist. Validate any post-login destination against a literal allowlist; never forward a user-provided URL without validation. Second, create a browser client and a server client in every SSR application. Supabase's official SSR documentation explains why the server-readable flow uses cookies instead of local storage and points SSR implementations to PKCE. The cookie adapter must read cookies from the request and write refreshed values to the response. Framework APIs evolve, so this is boundary-focused pseudocode rather than a drop-in adapter: const cookieOptions = { domain: ".example.com", path: "/", secure: true, sameSite: "lax" as const, httpOnly: true, } // Server-only pseudocode: use the current @supabase/ssr adapter for your framework. createServerClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, { cookies: { getAll: () => requestCookies.getAll(), setAll: (updates) => { for (const { name, value, options } of updates) { responseCookies.set(name, value, { ...options, ...cookieOptions }) } }, }, }) Domain=.example.com makes the cookie eligible for matching subdomains. Path=/ covers the whole site. Production requires HTTPS and Secure . HttpOnly prevents ordinary front-end JavaScript from reading the cookie, but it does not replace input sanitization or a content security policy. SameSite=Lax is often a reasonable starting point for same-site navigation. Use SameSite=None only when the actual cross-site flow requires it; MDN states that None must be paired with Secure . Test these choices against the real OAuth provider, embedding behavior, and CSRF controls instead of copying them blindly. Third, perform refresh work on server routes that are excluded from shared caches. The Supabase advanced guide warns that caching an authentication response containing Set-Cookie can replay one user's refreshed token to another user. Authentication middleware, callbacks, and personalized pages should be dynamic or private responses with appropriate Cache-Control . Do not treat merely decoding a JWT as final server authorization. Obtain a trustworthy user state and keep Row Level Security as the database authorization boundary. Fourth, design complete sign-out. Signing out from any subdomain should call the Supabase sign-out flow and expire the cookie with exactly the same Domain and Path used when creating it. Mismatched attributes can leave a second cookie with the same name, producing the illusion that one site ignored sign-out. Create separate cases for password reset, account suspension, refresh-token invalidation, and sign-out from all devices. Only then address CORS. Whether a browser sends a cookie is different from whether front-end JavaScript may read a response. A credentialed cross-origin API must return the exact permitted Access-Control-Allow-Origin ; credentialed requests cannot use wildcard * . The client must also request credentials. Sensitive mutations still need origin or CSRF validation even after CORS is correct. SameSite is defense in depth, not the sole defense. Failure and recovery One frequent symptom is a session that persists on one browser or subdomain but not another. Inspect the real Set-Cookie header in browser developer tools and compare Domain, Path, Secure, SameSite, and expiration before changing Auth code. If both a host-only cookie and a parent-domain cookie exist under the same name, remove both with their exact attributes in staging, then sign in again. Another common symptom is a refresh loop. Likely causes include a server adapter that cannot write the response cookie, different options in middleware and callback code, or a cache replaying an old response. Disable caching on authentication paths, attach request IDs, and log token expiry times without logging the token itself. Compare the request and response cookie changes across one refresh. If an untrusted subdomain receives the credential after release, stop using the parent Domain , expire the shared cookie, revoke affected sessions, and return to host-only cookies. The __Host- cookie prefix requires omitting Domain, so it is suitable for a stronger host-only boundary but cannot simultaneously implement parent-domain sharing. Prepare rollback before deployment. Keep the former single-site entry point, place shared-cookie behavior behind a feature flag, and make sure callbacks can return to the original host. After rollback, retest sign-in, refresh, sign-out, and session revocation. A working home page alone does not prove Auth recovery. Verification commands Use a dedicated staging account to run sign-in, refresh, and sign-out from each subdomain. These commands inspect response headers only; never print a real token into a terminal or CI log: curl -sS -D - -o /dev/null https://class.example.com/auth/callback curl -sS -I https://app.example.com/account The acceptance set s