Loading...
Loading...
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.
npx skill4agent add jezweb/claude-skills tanstack-start@tanstack/react-start@1.154.0# Create new project (uses Vite)
npm create cloudflare@latest my-app -- --framework=tanstack-start
cd my-app
# Install dependencies
npm install
# Development
npm run dev
# Build and deploy
npm run build
wrangler deploy{
"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"
}
}| Change | Old (Vinxi) | New (Vite) |
|---|---|---|
| Package name | | |
| Config file | | |
| API routes | | |
| Entry files | | |
| Source folder | | |
| Dev command | | |
# 1. Remove Vinxi
npm uninstall vinxi @tanstack/start
# 2. Install Vite and framework-specific adapter
npm install vite @tanstack/react-start @cloudflare/vite-plugin
# 3. Delete old config
rm app.config.ts
# 4. Delete default entry files (unless customized)
rm app/ssr.tsx app/client.tsx
# 5. Rename customized entries
mv app/ssr.tsx app/server.tsx # If you customized SSR entry
# 6. Move source files (optional, for consistency)
mv app/ src/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' } // Required for Workers
})
]
}){
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"start": "node .output/server/index.mjs"
}
}// Old (Vinxi)
import { createAPIFileRoute } from '@tanstack/start/api'
export const Route = createAPIFileRoute('/api/users')({
GET: async () => {
return { users: [] }
}
})
// New (Vite)
import { createServerFileRoute } from '@tanstack/react-start/api'
export const Route = createServerFileRoute('/api/users').methods({
GET: async () => {
return { users: [] }
}
})createAPIFileRoute()createServerFileRoute().methods()node_modules/package-lock.jsonapp.config.timestamp_*app.config.*name = "my-app"
compatibility_date = "2026-01-21"
compatibility_flags = ["nodejs_compat"] # REQUIRED
# REQUIRED: Point to TanStack Start's server entry
main = "@tanstack/react-start/server-entry"
[observability]
enabled = true # Optional: Enable monitoringimport { 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' } // REQUIRED
})
]
})# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
# KV Namespace
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"
# R2 Bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"import { createServerFn } from '@tanstack/react-start/server'
export const getUser = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
// D1
const result = await env.DB.prepare('SELECT * FROM users').all()
// KV
const value = await env.KV.get('key')
// R2
const object = await env.BUCKET.get('file.txt')
return result.results
})loadersexport const Route = createFileRoute('/users')({
loader: async () => {
// This route queries D1
},
// Disable prerendering
prerender: false
})wrangler dev# In CI environment
export CLOUDFLARE_INCLUDE_PROCESS_ENV=true
# Use .env file (NOT .env.local) for CI
# .env.local is gitignored and won't be in CIloader: async ({ context }) => {
// Skip DB queries during prerender
if (typeof context.cloudflare === 'undefined') {
return { users: [] }
}
const result = await context.cloudflare.env.DB.prepare('SELECT * FROM users').all()
return { users: result.results }
}@tanstack/react-start@1.138.0+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
})import { getUsers } from './server-functions'
function UserList() {
const users = await getUsers()
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}await request.formData()export const uploadFile = createServerFn()
.handler(async ({ request }) => {
// By the time this runs, the entire file is already in memory
const formData = await request.formData()
const file = formData.get('file') as File
// Too late to check size - file already loaded!
if (file.size > 10_000_000) {
throw new Error("File too large")
}
})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>
}undefinedconst login = createServerFn<{ username: string, password: string }, User>()
.handler(async ({ data, request }) => {
const user = await authenticateUser(data)
if (!user) {
// Redirect returns void, but type says it returns User
throw redirect({ to: '/login', status: 401 })
}
return user
})
// In component
const result = await login({ username, password })
// result is undefined if redirected, User object otherwise
// Check before using!
if (result) {
console.log(result.name)
}// This FAILS - cookies not forwarded
const getData = createServerFn()
.handler(async () => {
const response = await fetch('https://api.example.com/user')
// 401 Unauthorized - no cookies!
})import { createIsomorphicFn } from '@tanstack/react-start/server'
const getData = createIsomorphicFn()
.handler(async () => {
// Runs on client when possible, preserving cookies
const response = await fetch('https://api.example.com/user')
return response.json()
})import { createServerFn } from '@tanstack/react-start/server'
import { getRequestHeaders } from '@tanstack/react-start/server'
const getData = createServerFn()
.handler(async () => {
const headers = getRequestHeaders() // Get browser's original headers
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()
})createIsomorphicFnmultiSessionlastLoginMethodoneTapimport { betterAuth } from 'better-auth'
import { reactStartCookies } from 'better-auth/plugins'
export const auth = betterAuth({
plugins: [
reactStartCookies(), // Handles cookie setting for TanStack Start
],
// ... other config
})// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
engineType = "library"
runtime = "cloudflare" // or "workerd"
}generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}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()
})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
})drizzle-orm-d1import { createMiddleware } from '@tanstack/react-start/server'
const middleware = createMiddleware().server(async (ctx) => {
try {
const r = await ctx.next()
// Check for error in response object
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 })
}
})await request.formData()const result = await login({ username, password })
if (result) {
// Safe to use result
console.log(result.name)
}createIsomorphicFnruntime = "cloudflare"reactStartCookies()compatibility_flags = ["nodejs_compat"]process.env.NODE_ENV// This condition is statically evaluated and dead code eliminated
if (process.env.NODE_ENV === 'production') {
// Production-only code
} else {
// Development-only code (removed in prod build)
}routeTree.gen.tsautoCodeSplittingcloudflare-worker-basedrizzle-orm-d1ai-sdk-corereact-hook-form-zod