effect-auth

TanStack Start

Integrate effect-auth with a TanStack Start application.

TanStack Start can expose effect-auth through a native server route while the React application uses the typed browser client. The simplest deployment puts Start, CoreAuthHttpApi, and storage in one Worker. Requests stay same-origin, so the default client path and session cookie work without CORS configuration.

This guide follows the maintained tanstack-cloudflare-auth example. Start's APIs are still evolving; compare upgrades with the official Getting Started, Server Routes, Server Entry Point, and Cloudflare Workers documentation. The snippets below intentionally use server routes, not older createServerFn wiring.

File map

src/
  auth-client.ts          # typed client and session query
  routes/auth/$.ts        # GET/POST /auth/* server route
  routes/index.tsx        # sign-in UI and session rendering
  server/auth.ts          # effect-auth HttpApp and runtime layers
  server-entry.ts         # optional Worker entry and Durable Object export
  router.tsx              # Router + React Query SSR integration
vite.config.ts            # Start plugin; custom entry only when required

First complete the Quick Start to build handleAuthRequest(request). That function should run the effect-auth HttpApp with your database, cookie, mail, and rate-limit layers and return a standard Response. The framework-specific boundary is deliberately small.

Mount CoreAuthHttpApi

Create a splat server route. The file name and route ID are significant: $ captures everything below /auth/, including /auth/session and password endpoints.

src/routes/auth/$.ts
import { createFileRoute } from "@tanstack/react-router";

import { handleAuthRequest } from "../../server/auth";

export const Route = createFileRoute("/auth/$")({
  server: {
    handlers: {
      GET: ({ request }) => handleAuthRequest(request),
      POST: ({ request }) => handleAuthRequest(request),
    },
  },
});

Start passes the original Web Request to the handler and accepts the Web Response returned by effect-auth. Do not parse or reconstruct the request: auth bodies, origin headers, redirects, and Set-Cookie must pass through intact. Add another method only when an enabled endpoint actually requires it.

Create the client and session query

With /auth/* on the same origin, createAuthClient() needs no base URL. A React Query option centralizes the session contract and cancellation behavior:

src/auth-client.ts
import { createAuthClient } from "@effect-auth/core/Client";
import 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,
  });

See Browser Client for available operations. In a component, call useQuery(currentSessionQueryOptions()). After successful sign-in, invalidate authSessionQueryKey; after logout, setting that query to null gives immediate UI feedback. The API commits the HTTP-only session cookie, so application code should not store session tokens.

const sessionQuery = useQuery(currentSessionQueryOptions());
const session = sessionQuery.data ?? null;

const signIn = useMutation({
  mutationFn: (input: { email: string; password: string }) =>
    authClient.password.signIn(input),
  onSuccess: () =>
    queryClient.invalidateQueries({ queryKey: authSessionQueryKey }),
});

The maintained example creates one QueryClient per router and calls setupRouterSsrQueryIntegration({ router, queryClient }). This allows React Query state used during Start rendering to dehydrate and hydrate consistently. A component session query is useful for rendering account controls, but it is not an authorization boundary.

SSR and protected routes

TanStack Router's beforeLoad may run during the initial server render and during client navigation. If you protect a route there, resolve the session through request-aware server logic (including the incoming Cookie header), return it in router context, and throw redirect() when absent. Do not make an SSR guard depend on browser-only state or assume a relative browser fetch automatically carries the server render's request cookie.

On client navigation, the session query can drive UI gating because same-origin requests include the cookie. On the initial request, either render a neutral/loading shell until hydration or supply a server-resolved session to router context. In both cases, independently authorize every server route or backend operation that returns private data. beforeLoad protects navigation and rendered UI; users can call an endpoint without visiting that route. See TanStack Router's Authenticated Routes warning and patterns.

Cloudflare entry

Start provides a default server entry, so most applications do not need one. The maintained same-worker example configures tanstackStart({ server: { entry: "server-entry" } }) because its entry exports effect-auth's rate-limit Durable Object alongside Start's fetch handler. Keep that custom entry only if you need Worker exports or fetch customization; otherwise follow Start's current default and Cloudflare adapter documentation rather than copying infrastructure-specific code.

Split backend

The maintained tanstack-cloudflare-auth-split example deploys Start and CoreAuthHttpApi as separate Workers connected by a Cloudflare service binding. The browser still calls same-origin /auth/*; the Start route forwards the untouched request:

export const Route = createFileRoute("/auth/$")({
  server: {
    handlers: {
      GET: ({ request }) => env.AUTH_BACKEND.fetch(request),
      POST: ({ request }) => env.AUTH_BACKEND.fetch(request),
    },
  },
});

This preserves the simple client and cookie model while isolating auth resources. The split example also proxies /api/$ to the backend for protected application APIs. Prefer the same-worker topology until independent deployment, bindings, or security boundaries justify the extra Worker; direct cross-origin browser calls require deliberate cookie, origin, and CORS configuration and are not the canonical setup here.

On this page