Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add jezweb/claude-skills --skill "TanStack Start"
Install specific skill from multi-skill repository
# Description
|
# SKILL.md
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/[email protected] (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
- Migration from Vinxi to Vite
- Cloudflare Workers Deployment
- Server Functions
- Authentication Patterns
- Database Integration
- Known Issues Prevention
- Performance Optimization
Quick Start
Installation
# 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
{
"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
# 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/
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' } // Required for Workers
})
]
})
Update package.json Scripts
{
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"start": "node .output/server/index.mjs"
}
}
Update API Routes
// 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: [] }
}
})
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"] # REQUIRED
# REQUIRED: Point to TanStack Start's server entry
main = "@tanstack/react-start/server-entry"
[observability]
enabled = true # Optional: Enable monitoring
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' } // REQUIRED
})
]
})
Bindings (D1, KV, R2)
# 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"
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
// 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
})
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 () => {
// This route queries D1
},
// Disable prerendering
prerender: false
})
- Use remote bindings during builds (requires
wrangler devrunning):
# 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 CI
- Conditional logic in loaders:
loader: 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 }
}
Version Requirements:
- Static prerendering requires @tanstack/[email protected]+
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 }) => {
// 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")
}
})
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) {
// 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)
}
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.
// This FAILS - cookies not forwarded
const getData = createServerFn()
.handler(async () => {
const response = await fetch('https://api.example.com/user')
// 401 Unauthorized - no cookies!
})
Solution 1: Use createIsomorphicFn (runs on client when possible)
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()
})
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() // 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()
})
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:
1. Session cookie not re-set after expiry
2. Session token cookie issues with certain plugins (multiSession, lastLoginMethod, oneTap)
3. 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(), // Handles cookie setting for TanStack Start
],
// ... other config
})
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()
// 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 })
}
})
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:
1. Implement client-side file size validation
2. Use Cloudflare R2 multipart upload API directly for large files
3. 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) {
// Safe to use 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+)
// 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)
}
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
Additional Resources
Official Documentation:
- TanStack Start Docs
- Cloudflare Workers Guide
- TanStack Router Docs
Migration Guides:
- Official VinxiβVite Migration
- LogRocket Migration Article
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
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.