TanStack Start
SSR auth for TanStack Start with HttpOnly cookie sessions.
@vortex-api/convex-auth/tanstack-start and @vortex-api/convex-auth/tanstack-start/server are the TanStack Start SSR adapter. Sessions live in app-origin HttpOnly cookies; a request middleware lands OAuth/magic-link redirects, refreshes sessions near expiry, and serves the intent-based /api/auth proxy. It implements the SSR auth contract.
A working example lives in examples/tanstack-start.
Install
pnpm add @vortex-api/convex-auth convex
Point the app at your Convex deployment and wire the backend (component mount, convex/auth.ts, convex/http.ts) per Installation:
echo 'VITE_CONVEX_URL=https://<your-deployment>.convex.cloud' >> .env.local
Request middleware
Register convexAuthRequestMiddleware in src/start.ts:
// src/start.ts
import { createStart } from "@tanstack/react-start";
import { convexAuthRequestMiddleware } from "@vortex-api/convex-auth/tanstack-start/server";
import { api } from "../convex/_generated/api";
export const startInstance = createStart(() => ({
requestMiddleware: [convexAuthRequestMiddleware({ actions: api.auth })],
}));
The middleware intercepts auth requests and the /api/auth proxy internally — no separate route wiring is needed.
Options
| Option | Default | Description |
|---|---|---|
actions |
required | The api.auth object re-exporting convexAuth actions. |
convexUrl |
CONVEX_URL → VITE_CONVEX_URL → NEXT_PUBLIC_CONVEX_URL |
Convex deployment URL. |
apiRoute |
/api/auth |
Path that proxies auth actions. |
cookieConfig.maxAge |
null (session cookie) |
Cookie lifetime in seconds. Must be null or a positive number. |
requireLandingVerifier |
true |
Require OAuth/magic-link landings to carry a verifier param matching the cookie minted at flow start. Set false only for deployments that predate verifier threading. |
verbose |
false |
Debug logging. |
Server state + provider
Resolve auth state in the root route’s beforeLoad via a server function, then pass it to ConvexAuthTanstackStartProvider:
// src/lib/auth-server.ts
import { createServerFn } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
import {
getAuthServerState,
getConvexAuthSession,
} from "@vortex-api/convex-auth/tanstack-start/server";
import { api } from "../../convex/_generated/api";
/**
* Server-resolved auth state for the root route's `beforeLoad` — seeds the
* client provider and feeds route guards. Revocation-aware.
*/
export const getAuthState = createServerFn({ method: "GET" }).handler(async () => {
return await getAuthServerState(getRequest(), { actions: api.auth });
});
/**
* Verified session for protected loaders/server functions that don't want
* the full middleware context.
*/
export const getSession = createServerFn({ method: "GET" }).handler(async () => {
return await getConvexAuthSession(getRequest(), { actions: api.auth });
});
// src/routes/__root.tsx
import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ConvexAuthTanstackStartProvider } from "@vortex-api/convex-auth/tanstack-start";
import { api } from "../../convex/_generated/api";
import { getAuthState } from "../lib/auth-server";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
export const Route = createRootRoute({
beforeLoad: async () => ({
auth: await getAuthState(),
}),
component: RootComponent,
});
function RootComponent() {
const { auth } = Route.useRouteContext();
return (
<html lang="en">
<body>
<ConvexProvider client={convex}>
<ConvexAuthTanstackStartProvider actions={api.auth} serverState={auth}>
<Outlet />
</ConvexAuthTanstackStartProvider>
</ConvexProvider>
<Scripts />
</body>
</html>
);
}
Server helpers
From @vortex-api/convex-auth/tanstack-start/server:
| Helper | Returns | Notes |
|---|---|---|
getAuthServerState(request, { actions }) |
ConvexAuthServerState |
Server-resolved state for the provider’s serverState prop. |
getConvexAuthSession(request, { actions }) |
VerifiedSession |
Verified session — revocation-aware. |
getConvexAuthToken(request) |
string | null |
Raw session token. |
convexAuthCookieState(request) |
{ hasSessionCookie, tokenExpired } |
Optimistic boundary check — revocation-blind, pre-filtering only. |
convexAuthProxyHandler(options) |
(request) => Promise<Response> |
Factory — standalone proxy handler if you mount /api/auth outside the middleware. |
Route guards are UX, not security
beforeLoad redirects and convexAuthCookieState() checks shape navigation; they are not authorization. Always verify the session at the data boundary (getConvexAuthSession, verifySession, or ctx.auth.getUserIdentity() inside Convex functions).
Error params
Rejected landings redirect to /?error=landing_verifier_mismatch (verifier cookie absent or mismatched) or /?error=cross_origin (credentialed cross-origin request). Surface these in your UI — see the SSR auth contract for the full semantics.
Sign-in UI
Client components use the React client — sign-in calls mint the session through the /api/auth proxy into HttpOnly cookies:
// src/routes/sign-in.tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { FormEvent, useState } from "react";
import { useConvexAuthClient } from "@vortex-api/convex-auth/react";
export const Route = createFileRoute("/sign-in")({ component: SignInPage });
function SignInPage() {
const authClient = useConvexAuthClient();
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const result = await authClient.signIn.email({
email: String(data.get("email")),
password: String(data.get("password")),
});
if (result.error) {
setError(result.error.message ?? "Sign in failed");
return;
}
// 2FA-enrolled accounts resolve without a session — show the TOTP step.
if (result.data?.twoFactorRedirect) return;
// Re-run beforeLoad/loaders so route context picks up the fresh session.
await router.invalidate();
await router.navigate({ to: "/dashboard" });
};
const onGithub = async () => {
const result = await authClient.signIn.social({ provider: "github" });
// social() returns the authorization URL — it does not navigate for you.
if (result.data?.url) window.location.href = result.data.url;
};
return (
<form onSubmit={onSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign in</button>
<button type="button" onClick={onGithub}>
Continue with GitHub
</button>
{error ? <p role="alert">{error}</p> : null}
</form>
);
}
The full form (sign-up, redirect search param, guest) is in examples/tanstack-start/src/routes/sign-in.tsx. See the React guide for the complete hooks API.
Example app
examples/tanstack-start is a full SSR app on the adapter — the root route resolves the verified session in beforeLoad and seeds ConvexAuthTanstackStartProvider, a pathless _authed layout guards navigation, and protected createServerFns declare convexAuthFunctionMiddleware.
cd examples/tanstack-start
pnpm install
pnpm dlx convex dev # local backend on :3212
pnpm run dev # app on :3200