tanstack-start
Build full-stack React apps with TanStack Start — server functions, type-safe routing, loaders, middleware, SSR/streaming, and deployment patterns. Use when working on TanStack Start apps, server functions, TanStack Router, or any gremlin-cms development.
What this skill does
# TanStack Start
Full-stack React framework built on TanStack Router + Vite. Client-first with opt-in server capabilities. Type-safe from routes to server functions.
## When to Use
Triggers: `tanstack start`, `tanstack router`, `server function`, `createServerFn`, `createFileRoute`, `gremlin-cms`, `tanstack app`, or any work in a TanStack Start project.
## Core Concepts
### Execution Model — Critical
**Route loaders are ISOMORPHIC** — they run on BOTH server and client. This is the #1 gotcha.
```ts
// ❌ WRONG — loader runs on client too, exposes secrets
export const Route = createFileRoute('/users')({
loader: () => {
const secret = process.env.SECRET // Exposed to client!
return fetch(`/api/users?key=${secret}`)
},
})
// ✅ CORRECT — server function wraps server-only logic
const getUsers = createServerFn().handler(() => {
const secret = process.env.SECRET // Server-only
return fetch(`/api/users?key=${secret}`)
})
export const Route = createFileRoute('/users')({
loader: () => getUsers(), // Isomorphic call to server function
})
```
### Server Functions
Type-safe RPC that replaces REST/tRPC/GraphQL for internal data access. Build process replaces server implementations with RPC stubs in client bundles.
```ts
import { createServerFn } from '@tanstack/react-start'
// GET (default)
export const getPosts = createServerFn().handler(async () => {
return db.posts.findMany()
})
// POST with input validation
export const createPost = createServerFn({ method: 'POST' })
.inputValidator((data: { title: string; body: string }) => data)
.handler(async ({ data }) => {
return db.posts.create(data)
})
```
**Where to call server functions:**
- Route loaders — data fetching
- Components — via `useServerFn()` hook
- Other server functions — compose server logic
- Event handlers — form submissions, clicks
### Server-Only Functions
For utilities that must NEVER reach the client bundle:
```ts
import { createServerOnlyFn } from '@tanstack/react-start'
const getDbUrl = createServerOnlyFn(() => process.env.DATABASE_URL)
// Calling from client THROWS — crashes intentionally
```
### File-Based Routing
```
app/
├── routes/
│ ├── __root.tsx # Root layout
│ ├── index.tsx # /
│ ├── about.tsx # /about
│ ├── posts/
│ │ ├── index.tsx # /posts
│ │ └── $postId.tsx # /posts/:postId
│ └── _authed/
│ └── dashboard.tsx # /dashboard (with auth layout)
├── client.tsx # Client entry
├── router.tsx # Router config
└── ssr.tsx # SSR entry
```
- `$param` = dynamic segment
- `_prefix` = pathless layout route (groups routes without adding URL segments)
- `__root.tsx` = root layout (wraps everything)
### Route Definition
```ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// Loader runs before render (isomorphic!)
loader: ({ params }) => getPost({ data: params.postId }),
// Component receives loader data
component: PostPage,
// Error boundary
errorComponent: ({ error }) => <div>Error: {error.message}</div>,
// Pending component (while loader runs)
pendingComponent: () => <div>Loading...</div>,
})
function PostPage() {
const post = Route.useLoaderData()
return <h1>{post.title}</h1>
}
```
### Middleware
Compose reusable server function middleware:
```ts
import { createMiddleware } from '@tanstack/react-start'
const authMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
const session = await getSessionFn()
if (!session?.user) throw new Error('Unauthorized')
return next({ context: { session } })
}
)
// Use in server functions
export const listPosts = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
return db.posts.where({ userId: context.session.user.id })
})
```
### Server Routes (API endpoints)
```ts
export const Route = createFileRoute('/api/health')({
server: {
handlers: ({ createHandlers }) => createHandlers({
GET: async ({ request }) => {
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
})
},
}),
},
})
```
### Using with TanStack Query
```ts
import { useServerFn } from '@tanstack/react-start'
import { useQuery, useMutation } from '@tanstack/react-query'
function PostList() {
const getPostsFn = useServerFn(getPosts)
const createPostFn = useServerFn(createPost)
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => getPostsFn(),
})
const mutation = useMutation({
mutationFn: (data) => createPostFn({ data }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})
}
```
## File Organization (Large Apps)
```
src/utils/
├── users.functions.ts # createServerFn wrappers (safe to import anywhere)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe)
```
- `.functions.ts` — server function wrappers, safe to import anywhere
- `.server.ts` — server-only helpers, NEVER import from client code
## App Config
```ts
// app.config.ts
import { defineConfig } from '@tanstack/react-start/config'
import tsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
vite: {
plugins: [tsConfigPaths({ projects: ['./tsconfig.json'] })],
},
})
```
## BetterAuth Integration
```ts
// Server function for session
import { createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { auth } from '@/lib/auth'
export const getSessionFn = createServerFn({ method: 'GET' }).handler(
async () => {
const headers = getRequestHeaders()
return auth.api.getSession({ headers })
}
)
// Protected layout route
export const Route = createFileRoute('/_authed')({
beforeLoad: async () => {
const session = await getSessionFn()
if (!session?.user) throw redirect({ to: '/sign-in' })
},
component: () => <Outlet />,
})
```
## Deployment
TanStack Start deploys to any Node/Bun target, Vercel, Cloudflare Workers, Netlify.
### Vercel (Critical)
**You MUST add the `nitro()` Vite plugin** — without it, Vercel builds succeed but serve 404s.
```ts
// vite.config.ts
import { nitro } from 'nitro/vite'
export default defineConfig({
plugins: [
tanstackStart(),
nitro(), // ← REQUIRED for Vercel
viteReact(),
],
})
```
Install: `pnpm add nitro`
**Monorepo (pnpm + Turborepo) quirks:**
- Set `rootDirectory` in Vercel project settings to the app dir (e.g., `apps/gremlin-cms`)
- Framework preset should be **TanStack Start** or auto-detect — never Next.js
- Do NOT manually set `outputDirectory` — Nitro generates `.vercel/output` automatically
- Build command from repo root: `turbo run build --filter=<app-name>` or let Vercel auto-detect
- If you get 404 after successful build, check: (1) nitro plugin present, (2) framework preset correct, (3) no stale `.vercel` config
**No CLI deploys** — push to git, let Vercel auto-deploy. Only use `vercel --prod` for emergency hotfixes.
## Rules
1. **Never access `process.env` in loaders directly** — use `createServerFn` or `createServerOnlyFn`
2. **Loaders are isomorphic** — they run on both server AND client during navigation
3. **Server functions are the boundary** — anything that touches DB, env vars, or secrets goes through `createServerFn`
4. **Prefer server functions over API routes** for internal data access — type-safe, no manual fetch
5. **Use `useServerFn()` hook** when calling server functions from components (not direct calls)
6. **Middleware composes** — stack auth, validation, logging as reusable middleware
## Living Document
This skill will grow as we build gremlin-cms. Update with patterns discovered during development.
Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.