claude-start-cf

Execution Model

All code in TanStack Start is isomorphic by default — it runs on both server and client unless explicitly constrained. This page demonstrates each execution boundary.

APIRuns onIf called from wrong env
createServerFn()ServerNetwork request (RPC)
createServerOnlyFn()ServerThrows error
createClientOnlyFn()ClientThrows error
createIsomorphicFn()BothPicks correct impl
<ClientOnly>ClientRenders fallback
useHydrated()BothReturns false on server

createServerFn (RPC)

createServerFn({ method: 'GET' }).handler(async () => { ... })

Runs on the server. When called from the client, it becomes a network request automatically. Use for database access, secrets, and mutations.

RuntimeCloudflare Workers
Timestamp2026-02-20T10:09:13.889Z
Regionedge

createServerOnlyFn (hard boundary)

createServerOnlyFn(() => process.env.SECRET)

Unlike createServerFn, this has no RPC bridge. Calling it from the client throws an error immediately. Use for utility functions that must never leave the server — secret access, file system reads, etc.

// ✅ Safe — only callable from server code

const getDbUrl = createServerOnlyFn(() => process.env.DATABASE_URL)

// ❌ Calling from client crashes at runtime

const url = getDbUrl() // throws Error

When to choose this over createServerFn: Use createServerOnlyFn for pure utility functions that return secrets or do file I/O. Use createServerFn when you need the client to be able to call the function (it creates an RPC endpoint).

createClientOnlyFn (hard boundary)

createClientOnlyFn(() => ({ width: window.innerWidth, ... }))

The inverse of createServerOnlyFn. Calling it on the server throws an error. Use for browser API wrappers — localStorage, DOM measurements, geolocation, etc.

Waiting for client...

This function is only called inside useEffect so it never runs during SSR. Resize your browser to see it update.

Loader + Server Function

loader: () => getServerInfo()

Loaders are isomorphic — they run on both server (SSR) and client (navigation). Never access secrets directly in a loader. Delegate to a server function instead.

// ❌ loader runs on client too — secret leaks into bundle

loader: () => fetch(`/api?key=${process.env.SECRET}`)

// ✅ server function keeps secrets server-side

loader: () => getServerInfo()

createIsomorphicFn

createIsomorphicFn().server(() => ...).client(() => ...)

Provides different implementations depending on the environment. The framework picks the right one at runtime. Good for logging, storage adapters, and feature detection.

Currently running onserver

During SSR this shows "server". After hydration it shows "client". Unlike the hard boundary functions, this never throws — it always has an implementation for the current environment.

<ClientOnly> component

<ClientOnly fallback={...}>...</ClientOnly>

Renders children only after hydration. During SSR, renders the fallback. Use when an entire component tree needs browser APIs.

Waiting for hydration...

<ClientOnly> vs createClientOnlyFn: Use the component when you need to guard an entire subtree of JSX. Use the function when you need a callable utility.

useHydrated

const hydrated = useHydrated()

Returns false during SSR and first client render, then true after hydration. Useful for conditional rendering that avoids hydration mismatches.

Hydratedfalse
Live clockLoading...

The clock uses useEffect to avoid a hydration mismatch — the server and client would render different times otherwise.

Progressive Enhancement

Works without JS, enhanced with JS

Build forms that work as plain HTML, then enhance with client-side behavior. The form submits via GET without JavaScript; with JavaScript it can do instant search.

No JS yet — form still works via native HTML GET submission.