TanStack Start
Proxy a private effect-auth Worker through a same-origin TanStack Start route.
The canonical Cloudflare topology keeps the browser same-origin while TanStack Start forwards /auth/* to a private auth Worker through a service binding.
This guide hardens the maintained tanstack-cloudflare-auth-split shape by setting url: false. The current example still exports an auth backend URL, so add this setting before treating it as private. TanStack APIs evolve; the repository tests its pinned 1.168.x dependency graph.
| File | Responsibility |
|---|---|
alchemy.run.ts | D1, Durable Object, Workers, bindings |
src/routes/auth/$.ts | Transparent same-origin proxy |
src/auth/client.ts | Browser client and session query |
src/server/auth.ts | Effect Layers and cached web handler |
src/workers/auth-backend.ts | Private Worker entry and DO export |
Declare the topology
import * as Cloudflare from "alchemy/Cloudflare";
import * as Config from "effect/Config";
const AuthBackend = Cloudflare.Worker("AuthBackend", {
main: "./src/workers/auth-backend.ts",
url: false,
compatibility: { date: "2026-03-17", flags: ["nodejs_compat"] },
env: {
DB: Database,
RATE_LIMITER: RateLimiter,
AUTH_SECRET: Config.redacted("AUTH_SECRET"),
AUTH_PUBLIC_URL: Config.string("AUTH_PUBLIC_URL"),
},
});
const Website = Cloudflare.Website.Vite("Website", {
compatibility: { date: "2026-03-17", flags: ["nodejs_compat"] },
env: { AUTH_BACKEND: AuthBackend },
});url: false keeps auth off the public Internet. Bind D1, limiter, email, and secrets only to that Worker. Use a reviewed compatibility date supported by the pinned Alchemy release.
Forward the original request
import { createFileRoute } from "@tanstack/react-router";
import { env } from "../../server/env";
export const Route = createFileRoute("/auth/$")({
server: {
handlers: {
GET: ({ request }) => env.AUTH_BACKEND.fetch(request),
POST: ({ request }) => env.AUTH_BACKEND.fetch(request),
},
},
});Do not parse/re-encode bodies, reconstruct Request, copy headers into plain objects, or manufacture a new Response. Transparent forwarding preserves Cookie, Origin, streaming, redirects, and every Set-Cookie header.
Share the browser session query
import {
createAuthClient,
type CurrentSessionResponse,
} from "@effect-auth/core/Client";
import { queryOptions } from "@tanstack/react-query";
export const authClient = createAuthClient();
export const authSessionQueryKey = ["auth", "session"] as const;
export const currentSessionQueryOptions = () =>
queryOptions({
queryKey: authSessionQueryKey,
queryFn: async ({ signal }): Promise<CurrentSessionResponse | null> =>
(await authClient.session.currentOrUndefined({ signal })) ?? null,
});After sign-in, invalidate authSessionQueryKey; after logout, set it to null for immediate UI feedback. Never persist the HTTP-only session token in application state.
SSR and authorization
beforeLoad can run on server render and client navigation. Server-side guards must resolve the session from request-aware logic carrying the incoming cookie; a relative browser fetch does not automatically inherit the SSR request cookie. Either provide that session through router context or render a neutral shell until hydration.
| Boundary | What it protects |
|---|---|
| React/session query | Account controls and UI state |
Router beforeLoad | Navigation/rendering |
| Server route/operation authorization | Private data and mutations |
UI and router gates are not backend authorization. Independently authenticate and authorize every server operation.
Most Start apps use the default server entry. Add a custom entry only to export the rate-limit Durable Object or customize fetch handling; follow the current TanStack src/server.ts guidance when upgrading rather than copying a version-specific entry blindly.
Run local development through Alchemy so service bindings exist. Test the public website URL, session cookies, redirects, hostile origins, backend-binding failure, and multiple Set-Cookie propagation. See Split Frontend and Auth Workers and Cloudflare Workers.