| name | tanstack-start |
| description | Build full-stack React apps with TanStack Start on Cloudflare Workers. Type-safe routing, server functions, SSR/streaming, D1/KV/R2 integration.
Use when building full-stack React apps with SSR, migrating from Next.js, or from Vinxi to Vite (v1.121.0+). Prevents 9 documented errors including middleware bugs, file upload limitations, and deployment config issues.
|
| user-invocable | true |
| allowed-tools | ["Bash","Read","Write","Edit"] |
| metadata | {"package":"@tanstack/react-start","version":"1.154.0","last_verified":"2026-01-21","repository":"https://github.com/TanStack/router","documentation":"https://tanstack.com/start/latest","error_count":9} |
TanStack Start Skill
⚠️ Status: Production Ready (RC v1.154.0)
TanStack Start is a full-stack React framework built on TanStack Router. It provides type-safe routing, server functions, SSR/streaming, and first-class Cloudflare Workers support.
Current Package: @tanstack/react-start@1.154.0 (Jan 21, 2026)
Production Readiness:
- ✅ RC v1.154.0 stable (v1.0 expected soon)
- ✅ Memory leak issue (#5734) resolved Jan 5, 2026
- ✅ Migrated to Vite from Vinxi (v1.121.0, June 2025)
- ✅ Production deployments on Cloudflare Workers validated
This skill prevents 9 documented errors and provides comprehensive guidance for Cloudflare Workers deployment, migrations, and server function patterns.
Table of Contents
Quick Start
Installation
npm create cloudflare@latest my-app -- --framework=tanstack-start
cd my-app
npm install
npm run dev
npm run build
wrangler deploy
Dependencies
{
"dependencies": {
"@tanstack/react-start": "^1.154.0",
"@tanstack/react-router": "latest",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"vite": "latest",
"@cloudflare/vite-plugin": "latest",
"wrangler": "latest"
}
}
Migration from Vinxi to Vite (v1.121.0+)
Timeline: TanStack Start migrated from Vinxi to Vite in v1.121.0 (released June 10, 2025).
Breaking Changes
| Change | Old (Vinxi) | New (Vite) |
|---|
| Package name | @tanstack/start | @tanstack/react-start |
| Config file | app.config.ts | vite.config.ts |
| API routes | createAPIFileRoute() | createServerFileRoute().methods() |
| Entry files | ssr.tsx, client.tsx | server.tsx (optional) |
| Source folder | app/ | src/ |
| Dev command | vinxi dev | vite dev |
Migration Steps
npm uninstall vinxi @tanstack/start
npm install vite @tanstack/react-start @cloudflare/vite-plugin
rm app.config.ts
rm app/ssr.tsx app/client.tsx
mv app/ssr.tsx app/server.tsx
mv app/ src/
Create vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
tanstackStart(),
cloudflare({
viteEnvironment: { name: 'ssr' }
})
]
})
Update package.json Scripts
{
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"start": "node .output/server/index.mjs"
}
}
Update API Routes
import { createAPIFileRoute } from '@tanstack/start/api'
export const Route = createAPIFileRoute('/api/users')({
GET: async () => {
return { users: [] }
}
})
import { createServerFileRoute } from '@tanstack/react-start/api'
export const Route = createServerFileRoute('/api/users').methods({
GET: async () => {
return { users: [] }
}
})
Common Migration Errors
Error: "invariant failed: could not find the nearest match"
Cause: Old Vinxi route definitions mixed with Vite config
Fix: Update all createAPIFileRoute() → createServerFileRoute().methods()
Error: "SyntaxError: The requested module '@tanstack/router-generator' does not provide an export named 'CONSTANTS'"
Cause: Conflicting Vinxi/Vite dependencies
Fix: Delete node_modules/, package-lock.json, reinstall
Issue: Auto-generated app.config.timestamp_* files duplicating
Cause: Old Vinxi config interfering
Fix: Delete all app.config.* files, restart dev server
Reference: Official Migration Guide | LogRocket Migration Article
Cloudflare Workers Deployment
Required Configuration
wrangler.toml (or wrangler.jsonc)
name = "my-app"
compatibility_date = "2026-01-21"
compatibility_flags = ["nodejs_compat"]
main = "@tanstack/react-start/server-entry"
[observability]
enabled = true
vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
tanstackStart(),
cloudflare({
viteEnvironment: { name: 'ssr' }
})
]
})
Bindings (D1, KV, R2)
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
Access bindings in server functions:
import { createServerFn } from '@tanstack/react-start/server'
export const getUser = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const result = await env.DB.prepare('SELECT * FROM users').all()
const value = await env.KV.get('key')
const object = await env.BUCKET.get('file.txt')
return result.results
})
Prerendering Gotchas
Critical: Prerendering runs during build step using LOCAL environment variables, not Cloudflare bindings.
Problem: If routes use loaders that query D1/KV/R2, prerendering will fail because bindings aren't available at build time.
Solutions:
- Disable prerendering for routes with bindings:
export const Route = createFileRoute('/users')({
loader: async () => {
},
prerender: false
})
- Use remote bindings during builds (requires
wrangler dev running):
export CLOUDFLARE_INCLUDE_PROCESS_ENV=true
- Conditional logic in loaders:
loader: async ({ context }) => {
if (typeof context.cloudflare === 'undefined') {
return { users: [] }
}
const result = await context.cloudflare.env.DB.prepare('SELECT * FROM users').all()
return { users: result.results }
}
Version Requirements:
- Static prerendering requires
@tanstack/react-start@1.138.0+
Reference: Cloudflare Workers Guide
Server Functions
Server functions run on the server and can access Cloudflare bindings, databases, and secrets.
Basic Server Function
import { createServerFn } from '@tanstack/react-start/server'
export const getUsers = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const result = await env.DB.prepare('SELECT * FROM users').all()
return result.results
})
Use in Components
import { getUsers } from './server-functions'
function UserList() {
const users = await getUsers()
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
File Upload Limitation
⚠️ Known Issue: TanStack Start automatically calls await request.formData() for multipart/form-data requests, loading entire files into memory BEFORE the handler runs.
Impact:
- Cannot enforce upload size limits before loading
- Cannot implement streaming uploads
- Large file uploads consume excessive memory
Example of the Problem:
export const uploadFile = createServerFn()
.handler(async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file') as File
if (file.size > 10_000_000) {
throw new Error("File too large")
}
})
Workarounds:
- Client-side validation (not foolproof, can be bypassed):
function FileUpload() {
const handleSubmit = async (e: FormEvent) => {
const file = e.currentTarget.querySelector('input[type="file"]').files[0]
if (file.size > 10_000_000) {
alert("File too large")
return
}
await uploadFile({ file })
}
return <form onSubmit={handleSubmit}>...</form>
}
- Use Cloudflare R2 multipart upload API directly for large files (bypasses Start's form handling).
Status: Open issue #5704, no fix planned yet.
Server Function Redirects Return Undefined
When a server function performs a redirect, the promise resolves to undefined instead of the declared return type.
const login = createServerFn<{ username: string, password: string }, User>()
.handler(async ({ data, request }) => {
const user = await authenticateUser(data)
if (!user) {
throw redirect({ to: '/login', status: 401 })
}
return user
})
const result = await login({ username, password })
if (result) {
console.log(result.name)
}
Prevention: Always check return value before use if server function can redirect.
Status: Open PR #6295 to fix return type.
Authentication Patterns
Stateful Backend Integration (Laravel Sanctum, etc.)
Problem: When using stateful backends, server functions lose auth context because requests originate from the Start server, not the browser. Cookies, CSRF tokens, and origin headers are missing.
const getData = createServerFn()
.handler(async () => {
const response = await fetch('https://api.example.com/user')
})
Solution 1: Use createIsomorphicFn (runs on client when possible)
import { createIsomorphicFn } from '@tanstack/react-start/server'
const getData = createIsomorphicFn()
.handler(async () => {
const response = await fetch('https://api.example.com/user')
return response.json()
})
Solution 2: Manual Header Forwarding
import { createServerFn } from '@tanstack/react-start/server'
import { getRequestHeaders } from '@tanstack/react-start/server'
const getData = createServerFn()
.handler(async () => {
const headers = getRequestHeaders()
const response = await fetch('https://api.example.com/user', {
headers: {
'Cookie': headers.get('cookie') || '',
'X-XSRF-TOKEN': headers.get('x-xsrf-token') || '',
'Origin': headers.get('origin') || '',
}
})
return response.json()
})
When to Use Each:
createIsomorphicFn: Best for read operations, maintains full browser context
- Manual forwarding: Required for operations that must run server-side (secrets, DB access)
Reference: GitHub Discussion #6289
Better Auth Integration
Issue: Better Auth cookie caching has edge cases with TanStack Start:
- Session cookie not re-set after expiry
- Session token cookie issues with certain plugins (
multiSession, lastLoginMethod, oneTap)
- Hard reload/direct URL doesn't read cookies (works with client navigation only)
Solution: Use Better Auth's official TanStack Start plugin
import { betterAuth } from 'better-auth'
import { reactStartCookies } from 'better-auth/plugins'
export const auth = betterAuth({
plugins: [
reactStartCookies(),
],
})
Known Limitations:
- Some edge cases remain with hard reloads
- Session cookie re-setting after expiry may not work consistently
References: Issue #4389, Issue #5639
Database Integration
Prisma with Cloudflare Workers
Issue: Deploying with Prisma Edge fails with "No such module 'assets/.prisma/client/edge'" error.
Solution: Configure Prisma for Cloudflare runtime
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
engineType = "library"
runtime = "cloudflare" // or "workerd"
}
Alternative Configuration:
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Then use with Cloudflare Hyperdrive:
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import { Pool } from 'pg'
export const getUser = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString })
const adapter = new PrismaPg(pool)
const prisma = new PrismaClient({ adapter })
return prisma.user.findMany()
})
Reference: Cloudflare Workers SDK Issue #10969
D1 Database
export const getUsers = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const result = await env.DB.prepare('SELECT * FROM users').all()
return result.results
})
Use with drizzle-orm-d1 skill for type-safe ORM.
Known Issues Prevention
This skill prevents 9 documented issues:
Issue #1: Middleware Does Not Catch Server Function Errors
Error: Errors thrown by server functions bypass middleware try-catch blocks
Source: GitHub Issue #6381
Status: Fixed in v1.155+ (expected release)
Why It Happens: Server function errors are returned as error objects in the response, not thrown directly.
Prevention (workaround for v1.154 and earlier):
import { createMiddleware } from '@tanstack/react-start/server'
const middleware = createMiddleware().server(async (ctx) => {
try {
const r = await ctx.next()
if ('error' in r && r.error) {
throw r.error
}
return r
} catch (error: any) {
console.error("Middleware caught an error:", error)
return new Response("An error occurred", { status: 500 })
}
})
Issue #2: File Upload Streaming Not Supported
Error: Large file uploads consume excessive memory
Source: GitHub Issue #5704
Status: Open, no fix planned
Why It Happens: Framework automatically calls await request.formData() before handler runs, loading entire file into memory.
Prevention:
- Implement client-side file size validation
- Use Cloudflare R2 multipart upload API directly for large files
- Set reasonable file size limits in upload UI
See File Upload Limitation section for details.
Issue #3: Server Function Redirects Return Undefined
Error: Type errors when using server function result after redirect
Source: GitHub PR #6295
Status: Open PR
Why It Happens: Redirects return void, but return type doesn't reflect this.
Prevention: Always check server function return value before use
const result = await login({ username, password })
if (result) {
console.log(result.name)
}
Issue #4: Stateful Auth Cookies Not Forwarded
Error: 401 Unauthorized when calling stateful backend APIs from server functions
Source: GitHub Discussion #6289
Why It Happens: Server functions originate from Start server, not browser, so cookies aren't forwarded.
Prevention: Use createIsomorphicFn or manual header forwarding
See Stateful Backend Integration section.
Issue #5: Prisma Edge Module Not Found
Error: "No such module 'assets/.prisma/client/edge'"
Source: Cloudflare Workers SDK Issue #10969
Status: Resolved with runtime config
Why It Happens: Prisma Edge client not properly bundled for Workers environment.
Prevention: Configure Prisma with runtime = "cloudflare" in schema.prisma
See Prisma with Cloudflare Workers section.
Issue #6: Better Auth Cookie Caching Issues
Error: Session cookies not set/refreshed properly
Source: Better Auth Issues #4389, #5639
Why It Happens: Better Auth's default cookie handling doesn't account for Start's execution model.
Prevention: Use reactStartCookies() plugin
See Better Auth Integration section.
Issue #7: Missing nodejs_compat Flag
Error: Runtime errors when using Node.js APIs on Cloudflare Workers
Source: Cloudflare Workers Guide
Why It Happens: TanStack Start uses Node.js APIs that require compatibility flag.
Prevention: Add compatibility_flags = ["nodejs_compat"] to wrangler.toml
Issue #8: Prerendering Fails with Cloudflare Bindings
Error: Build fails when routes with loaders use D1/KV/R2
Source: Cloudflare Workers Guide
Why It Happens: Prerendering runs at build time without access to Cloudflare bindings.
Prevention: Disable prerendering for routes with bindings, or use conditional logic
See Prerendering Gotchas section.
Issue #9: Vinxi Migration Errors
Error: "invariant failed: could not find the nearest match" after upgrading to v1.121.0+
Source: Release v1.121.0
Why It Happens: v1.121.0 migrated from Vinxi to Vite with breaking changes.
Prevention: Follow complete migration guide
See Migration from Vinxi to Vite section.
Performance Optimization
Static Process.env Replacement
Feature: Build-time replacement of process.env.NODE_ENV for better optimization (v1.154.0+)
if (process.env.NODE_ENV === 'production') {
} else {
}
Automatic: No configuration needed, works out of the box.
Development Performance with Many Routes
Issue: Apps with 100+ routes generate 700+ HTTP requests in Vite dev mode.
Why: routeTree.gen.ts statically imports every route for type generation, even though autoCodeSplitting is enabled by default.
Impact: Slow dev server, hits proxy rate limits (ngrok 360 req/min)
Status: Expected behavior until Router v2. Not a bug, architectural limitation.
Workarounds:
- Use production builds for testing with many routes
- Reduce route count during development
- Use local tunneling without rate limits (Cloudflare Tunnel instead of ngrok)
Reference: GitHub Discussion #6353
TanStack Query Integration
Use TanStack Query with route loaders so that data is prefetched in the loader and consumed in the component via useSuspenseQuery. This keeps the router in a pending state while the loader promise is unresolved, which allows navigation progress indicators (e.g. a top-of-page progress bar) to run on route transitions.
Setup
-
Router context: Create a QueryClient and pass it in router context. Export a RouterContext type that includes queryClient: QueryClient.
-
Root route: Use createRootRouteWithContext<RouterContext>() instead of createRootRoute so child route loaders receive context.queryClient.
Pattern
- Define query options with
queryOptions({ queryKey, queryFn }) (shared between loader and component).
- Loader: Call
context.queryClient.ensureQueryData(queryOptions) so data is fetched and cached before the route renders; the loader returns a promise, so the router stays pending until data is ready.
- Component: Use
useSuspenseQuery(queryOptions) to read the cached data (no extra request when the loader already ran).
- After mutations: Call
queryClient.invalidateQueries({ queryKey }) to refetch and update the UI.
export type RouterContext = { queryClient: QueryClient };
const itemsQueryOptions = queryOptions({
queryKey: ["items"],
queryFn: () => getItems(),
});
export const Route = createFileRoute("/dashboard")({
loader: ({ context }) =>
context.queryClient.ensureQueryData(itemsQueryOptions),
component: Page,
});
function Page() {
const { data: items } = useSuspenseQuery(itemsQueryOptions);
}
When the user navigates to the route, the loader runs, the router remains pending until ensureQueryData resolves, and any component that observes routerState.status === "pending" (e.g. NavProgress) can show a progress indicator.
Additional Resources
Official Documentation:
Migration Guides:
Related Skills:
cloudflare-worker-base - Cloudflare Workers deployment patterns
drizzle-orm-d1 - Type-safe D1 database access
ai-sdk-core - AI integration with server functions
react-hook-form-zod - Form handling with validation
Last verified: 2026-01-21 | Skill version: 2.0.0 | Changes: Expanded from draft with 9 documented issues, migration guide, Cloudflare deployment, auth patterns, and database integration