TanStack Start
One SSR-safe provider for TanStack Start, no client boundary. Session mode gets a cookie transport and an authenticated first paint.
Finish Quick start steps 1 to 3, then mount the provider. TanStack Start renders on the server, and both providers are SSR-safe, so there is no client boundary to add. Use the same single provider you'd use in a SPA.
Session mode
On the server ConvexLogtoSessionProvider renders one fixed restoring
snapshot and touches no browser API. It mounts in the same place as in a SPA.
import { ConvexReactClient } from "convex/react";
import { ConvexLogtoSessionProvider } from "convex-logto/react-session";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL, {
initialAuthTokenReuse: true,
});
<ConvexLogtoSessionProvider
client={convex}
sessionApi={api.auth}
navigate={(to) => void router.navigate({ to, replace: true })}
>
<RouterProvider router={router} />
</ConvexLogtoSessionProvider>;Callback route
Keep the /callback route; the provider finishes the exchange there before
navigating on.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/callback")({
component: () => <p>Finishing sign in…</p>,
});The pieces that are Start-specific
Start has a server on the same origin, which is what the two optional session-mode features need.
-
Mount the cookie handler as a server route. Start's server handlers receive a standard
Request, which is what the handler takes:src/routes/api/logto/$.ts import { createFileRoute } from "@tanstack/react-router"; import { logtoCookieHandler } from "~/server/logto-cookie"; export const Route = createFileRoute("/api/logto/$")({ server: { handlers: { POST: ({ request }) => logtoCookieHandler(request), OPTIONS: ({ request }) => logtoCookieHandler(request), }, }, });Then pass
cookieTransport={{ endpoint: "/api/logto" }}to the provider. The rotating session token moves into an HttpOnly__Host-cookie that JavaScript cannot read. See the cookie transport for the handler module and itsallowedOrigins. -
Seed the first paint. With the cookie in place, a server function can exchange it for a fresh ID token during the root loader, so the first render is already authenticated:
const seedSession = createServerFn().handler(async () => { const seed = await logtoCookieHandler.getInitialToken(getRequest()); for (const [name, value] of seed.headers) setResponseHeader(name, value); return { initialToken: seed.initialToken, initialSessionId: seed.initialSessionId, }; });Pass the pair to
initialToken/initialSessionId. Forwardingseed.headersis not optional. That response carries the rotated cookie. Dropping it leaves the browser holding a superseded token, which it will eventually present outside the reuse window, and the component will read that as reuse. Call it at most once per document request.
Full walkthrough: Session mode.
Bridge mode
ConvexLogtoProvider is SSR-safe too. With static config nothing touches
window during render, so it mounts in the same place with no client boundary.
With configQuery it renders fallback (default null) on the server and on
the first client paint, and mounts children once the query resolves.
import { ConvexLogtoProvider } from "convex-logto/react";
<ConvexLogtoProvider
client={convex}
config={{
endpoint: import.meta.env.VITE_LOGTO_ENDPOINT,
appId: import.meta.env.VITE_LOGTO_APP_ID,
}}
navigate={(to) => void router.navigate({ to, replace: true })}
>
<RouterProvider router={router} />
</ConvexLogtoProvider>;The /callback route is the same one shown above.
Full app: examples/tanstack-start.