TanStack Start + Supabase Auth: The Production Setup (2026)
Jimmy Smith
Production Supabase Auth on TanStack Start needs four things most quickstarts skip: a cookie-correct @supabase/ssr server client wired to @tanstack/react-start/server, supabase.auth.getClaims() for a verified JWT on every request, beforeLoad guards plus a real RBAC middleware stack for server functions, and RLS-scoped Postgres RPCs so data access doesn't leak into hand-rolled account_id filters. We ship all of it today in the TanStack Start SaaS kit's Supabase variant, and every code sample below is pulled from that codebase, not sketched from the docs.
The Supabase official quickstart shows you read-only CRUD with no auth. The Convex integration punts auth to a third party. A couple of strong blog posts each cover half of the real setup: one nails the cookie-correct server client but skips route protection, the other has the right security mental model but never mentions Supabase or RLS. Nobody puts the whole production path, including MFA and role-based access, in one place. That's what this post is, and it's not theoretical: it's the auth layer of a shipping product.
This is written against TanStack Start on Vite + Nitro with @supabase/ssr current and React 19, tested July 2026 against our own kit's 1.0.0 release. TanStack Start is still young and moving fast, so date anything you copy from tutorials, including this one.
New to the framework? Start with TanStack Start vs Next.js for the mental model, then come back.
Why the quickstarts aren't enough
The gap isn't that good material doesn't exist, it's that no single source connects the pieces, and the pieces interlock:
- Supabase's official quickstart: canonical and correct as far as it goes, but it's read-only CRUD with RLS on by default and no auth. No
@supabase/ssrsession, nocreateServerFn, nobeforeLoad. - The Convex integration: ranks well, but has no RLS model at all and punts authentication to Clerk or WorkOS. Not a Supabase reference.
- The strongest Supabase-specific blog posts: get the cookie-correct
createServerClientright, which is the hardest part, but stop at "you're logged in." No route protection, no server-function authorization, no roles. - The best auth-guide posts: have the right security mental model (server function is the boundary, guard twice) but never touch Supabase,
@supabase/ssr, or RLS.
Merge the security model with the Supabase specifics, add protected routing, RBAC, MFA, and RLS, and you have a setup that survives contact with real users. Let's build it.
The mental model: your server function is the security boundary
Start here, because getting this wrong is what makes "authenticated" apps leak data.
TanStack Start has no middleware.ts and no Next.js-style proxy sitting in front of every route. There's a global request middleware you register yourself, plus route-level beforeLoad guards, plus server functions. The critical thing to internalize: a beforeLoad guard protects navigation, not data. It decides whether a user is allowed to see a screen. It does nothing to protect the endpoints that screen calls.
Server functions (createServerFn) are directly-callable POST RPCs. Anyone who can reach your app can invoke one directly, with or without ever loading the route that normally calls it. They run through the request pipeline like any other POST, which is exactly why each one has to authorize itself. Nobody is doing it for them.
So you guard twice, and the two guards do different jobs:
beforeLoadguard = UX. Redirect anonymous users to sign-in before they see a protected screen. Fast, good experience, not a security control.- Server function auth = security. Every server function that reads or writes private data checks the user itself, or runs through middleware that does. This is the real boundary.
Our kit's Supabase variant encodes exactly this split, and it goes further than a single guard function: it's a composable RBAC middleware stack that every server function opts into by construction. That's the part most "here's how to add Supabase to TanStack Start" posts skip entirely, because they stop at "check if there's a user."
Setting up the @supabase/ssr server client
The server client is the load-bearing piece. It has to read auth cookies off the incoming request and write refreshed cookies back on the response, through TanStack Start's server surface, not Next.js helpers.
How to set up the Supabase SSR server client in TanStack Start:
- Create a Supabase project and grab the project URL and publishable (anon) key.
- Install
@supabase/ssrand@supabase/supabase-js. - Build a server client with
createServerClient, passinggetAll/setAllcookie accessors. - Source those cookies from
@tanstack/react-start/server(getRequest(),setCookie), parsed withparseCookieHeader. - Gate
cookieOptions.secureonNODE_ENV === 'production'so localhttp://localhostdev keeps working.
Here's the client, unchanged from packages/supabase/src/clients/server-client.server.ts in the kit:
import { createServerClient, parseCookieHeader } from '@supabase/ssr';
import { getRequest, setCookie } from '@tanstack/react-start/server';
import { type Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
export function getSupabaseServerClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.publicKey, {
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
cookies: {
getAll() {
const header = getRequest().headers.get('cookie') ?? '';
return parseCookieHeader(header).map(({ name, value }) => ({
name,
value: value ?? '',
}));
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
setCookie(name, value, options),
);
},
},
});
}