Every module is isomorphic by default: it runs in BOTH server and client bundles, and route loaders run on both server (SSR) and client (navigation) — so DB access, filesystem, and secrets must live inside createServerFn or createServerOnlyFn, never a bare loader or module scope. process.env must be read per request, not at module scope, because module-level reads can leak into the client bundle and are undefined on edge runtimes that inject env at request time. VITE_/PUBLIC_-prefixed vars are exposed to the client, so server secrets must never carry that prefix.
Documents every environment-boundary API at a glance — createServerFn, createServerOnlyFn, createClientOnlyFn, createIsomorphicFn, , useHydrated(), and the server-only/client-only import markers — with a decision framework for which to use and complete copy-paste boundary examples.
Do NOT use this skill for initial project setup (use start-core) or for file/directory SOLID organization (use solid-tanstack-start).
TanStack Start Execution Model
Understanding where code runs is the single most important concept in Start,
and the #1 source of AI-generated bugs. Targets @tanstack/react-start v1.166.2.
CRITICAL — everything is isomorphic by default. Every module runs in BOTH the server and client bundles. Route loaders run on both — during SSR AND during client navigation. DB, filesystem, and secrets MUST live inside
createServerFn(orcreateServerOnlyFn), never a bare loader or module scope.CRITICAL — read
process.envper request, not at module scope. Module-level reads (1) can be inlined into the client bundle (secret leak) and (2) areundefinedon edge runtimes (Cloudflare Workers inject env at request time).CRITICAL —
VITE_/PUBLIC_prefixed vars are exposed to the client. Server secrets must have NO public prefix.
Agent Workflow (MANDATORY)
Verify APIs against Context7 (/websites/tanstack_start_framework_react) + Exa
before writing boundary code. After changes, run fuse-ai-pilot:sniper.
The Boundary APIs at a Glance
| API | Use case | Client behavior | Server behavior |
|---|---|---|---|
createServerFn() |
RPC, data, mutations | network request | direct execution |
createServerOnlyFn(fn) |
server utility | throws | direct execution |
createClientOnlyFn(fn) |
browser utility | direct execution | throws |
createIsomorphicFn() |
per-env implementation | .client() impl |
.server() impl |
<ClientOnly fallback> |
browser-only UI | renders children | renders fallback |
useHydrated() |
post-hydration logic | true after hydrate |
false |
import '@tanstack/react-start/server-only' |
whole file server-only | import denied | allowed |
import '@tanstack/react-start/client-only' |
whole file client-only | allowed | import denied |
All boundary creators are imported from @tanstack/react-start; <ClientOnly>
and useHydrated come from @tanstack/react-router.
The One Gotcha to Remember
// ❌ WRONG — loader is isomorphic; SECRET ships to the browser
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const secret = process.env.API_SECRET // leaked
return fetch(url, { headers: { Authorization: secret } })
},
})
// ✅ CORRECT — server-only work behind createServerFn
const getData = createServerFn({ method: 'GET' }).handler(async () => {
const secret = process.env.API_SECRET // stays on the server
return fetch(url, { headers: { Authorization: secret } })
})
export const Route = createFileRoute('/dashboard')({ loader: () => getData() })
Reference Guide
| Topic | Reference | Load when |
|---|---|---|
| Why loaders/modules are isomorphic; the mental model | references/isomorphic-by-default.md |
deciding server vs client vs both |
| Each boundary API + import markers + which to pick | references/environment-boundaries.md |
choosing an API, marking a file |
VITE_/PUBLIC_ vs process.env, per-request reads, runtime vars |
references/environment-variables.md |
env var undefined / leaked |
| Complete copy-paste examples of every boundary | references/templates/boundaries.md |
writing boundary code |
Decision Framework
- Server-only (
createServerFn/createServerOnlyFn): secrets, DB, filesystem, external API keys. - Client-only (
createClientOnlyFn/<ClientOnly>): DOM,localStorage, geolocation, analytics. - Isomorphic (default /
createIsomorphicFn): formatting, business logic, shared utilities, loaders.
Forbidden
- DB / secrets / filesystem in a bare loader (→
createServerFn). process.envat module scope (leak +undefinedon edge).VITE_/PUBLIC_prefix on a secret.- Non-deterministic render output (
new Date()directly) → hydration mismatch. - Dynamic
import()of a*.functions.tsserver function (bundler issues).