---
name: tanstack-start-cloudflare
description: "Use when building routes, layouts, breadcrumbs, Fast Refresh-safe boundaries, server functions, error handling, metadata, or deployment config in a TanStack Start app deployed to Cloudflare Workers."
---
## Routing
File-based routing via TanStack Router. Router context type: `{ queryClient, session, user }`.
`getRouter()` must create a new router instance per request/session. Do not import a singleton router into server code.
Root `beforeLoad` primes auth via `ensureQueryData`. Route loaders prime cache with `ensureQueryData`, components read via `useQuery()` — NEVER `useLoaderData()`.
Links: `` — never template strings.
Never run TanStack Router CLI generation manually — `bun run dev` generates the route tree automatically.
## Fast Refresh Boundaries
When TanStack Start SSR runs through `@cloudflare/vite-plugin`, the SSR environment is backed by workerd/Miniflare. Bad HMR invalidation is expensive because SSR updates swap a Worker runtime, not a plain Node module.
TSX files that export React components or hooks must not also export runtime helpers. In route TSX modules, the only tolerated route export is TanStack `Route`.
Do not export these from React TSX modules:
- `validateSearch` schemas
- loaders or preload functions
- route data helpers
- `head` or metadata builders
- constants used by routes
- tab value arrays and guards
- class or variant helpers
- label, title, or path builders
- server functions
Move those helpers into colocated `.ts` modules named by responsibility:
- `route-data.ts`
- `*-loader.ts`
- `*-search.ts`
- `*.variants.ts`
- `*.styles.ts`
- `*-labels.ts`
- `*-meta.ts`
Avoid vague module names like `copy`, `utils`, or `helpers` unless the existing local pattern already uses them.
Before finishing route or component work, run `bun check:fast-refresh`, then the repo's normal smallest relevant check, usually `bun lint:check` or `bun check`. If the guard fails, split the TSX export boundary instead of suppressing the check.
## Layouts
Two layout routes:
1. **`_auth.tsx`** — guest-only. Redirects to `/dashboard` if logged in. Renders `` (centered card, no chrome).
2. **`_authed.tsx`** — protected. Redirects to `/login?redirect=current` if not logged in. Renders `` + `` with breadcrumbs.
Layout components live in `src/components/app/`: `app-sidebar.tsx`, `app-topbar.tsx`, `auth-layout.tsx`.
## Breadcrumbs
No `useEffect`, no global state. Routes declare breadcrumbs in `staticData`. Dynamic labels via `routeContext` in `beforeLoad`. Keep route data implementation in colocated `.ts` files and have the route TSX wire those functions into `Route`.
```tsx
// Route definition with breadcrumb
import { getProjectRouteContext, loadProjectRoute } from './project-route-data'
export const Route = createFileRoute('/_authed/projects/$id')({
staticData: { breadcrumb: 'Project Details' },
beforeLoad: getProjectRouteContext,
loader: loadProjectRoute,
})
```
```tsx
// useBreadcrumbs hook
function useBreadcrumbs() {
const matches = useMatches()
return matches
.filter((m) => m.staticData?.breadcrumb || m.context?.breadcrumb)
.map((m) => ({
label: m.context?.breadcrumb ?? m.staticData.breadcrumb,
path: m.pathname,
}))
}
```
`` renders inside `AppTopBar` automatically.
## Metadata & Head
Root route sets defaults: charset, viewport, title, description, theme-color, favicon links, manifest. Per-route title overrides via `head` function. Put nontrivial title, label, and metadata builders in a colocated `*-meta.ts` module and wire them into `Route`. Static assets in `public/`: `favicon.ico`, `icon.svg`, `apple-touch-icon.png`, `icon-192.png`, `icon-512.png`, `manifest.webmanifest`.
## Server Functions
Server functions are callable across the network boundary. Treat every input as untrusted even when a route guard exists.
Never declare `createServerFn` in `.tsx`. Put server functions in the API/data layer or a `.ts` route-data module. Do not paper over client/server boundary issues with dynamic imports; fix the import graph.
```ts
// src/api/items/functions.ts or src/routes/-items/route-data.ts
import { createServerFn } from '@tanstack/react-start'
export const getItems = createServerFn({ method: 'GET' })
.inputValidator(z.object({ cursor: z.string().optional() }))
.handler(async ({ input }) => {
const supabase = getSupabaseServerClient()
const { data, error } = await supabase.from('items').select('*')
if (error) throw error
return data
})
```
- Input validation: `.inputValidator(schema)` — NEVER `.validator()`.
- Errors: `throw new Error(message)` — caught by global `MutationCache` → toast.
- Route guards improve UX, not security. Sensitive server functions must enforce authorization through Supabase RLS, scoped queries, or explicit server-side session/role checks.
- Do not duplicate auth checks only when a shared server helper already proves the same invariant and RLS still protects the table.
- Always use `getSupabaseServerClient()` — never `createClient()` or the browser client.
## Error Handling
- Route-level `errorComponent` on layout routes (`_authed.tsx`, `_auth.tsx`).
- Global `notFoundComponent` on root route.
- Per-route `errorComponent` overrides as needed.
## Vite Config
```ts
import { cloudflare } from '@cloudflare/vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tailwindcss(),
tanstackStart(),
react(),
],
})
```
NEVER use `app.config.ts` or vinxi.
## Deployment
Cloudflare Workers only (not Pages). Config in `wrangler.jsonc`. Deploy scripts live in CI — not in package.json.