Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/TanStack/router/llms.txt

Use this file to discover all available pages before exploring further.

Deployment

TanStack Start applications can be deployed to any hosting platform that supports Node.js. This guide covers deployment strategies, platform-specific configurations, and production optimizations.

Build Process

Before deploying, build your application for production:
# Build the application
pnpm build

# Output structure:
# .output/
#   ├── public/          # Static assets
#   │   ├── assets/      # JS, CSS bundles
#   │   └── *.html       # Static pages (if prerendered)
#   └── server/          # Server bundle
#       └── index.mjs    # Server entry point
The build process:
  1. Bundles client code - Creates optimized JavaScript bundles
  2. Bundles server code - Creates a production server bundle
  3. Generates manifests - Creates asset manifests for SSR
  4. Optimizes assets - Minifies and compresses static files
  5. Code splitting - Splits code by route for optimal loading

Hosting Platforms

Nitro-based Deployment

TanStack Start uses Nitro for universal deployment:
// vite.config.ts
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
import { nitro } from 'nitro/vite'

export default defineConfig({
  plugins: [
    tanstackStart(),
    nitro({
      // Nitro configuration
    }),
  ],
})
Reference: examples/react/start-basic/vite.config.ts:1-22

Vercel

Deploy to Vercel with zero configuration:
// package.json
{
  "scripts": {
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}
// vercel.json (optional)
{
  "buildCommand": "pnpm build",
  "outputDirectory": ".output/public",
  "framework": null
}
Deployment steps:
  1. Install Vercel CLI: pnpm add -g vercel
  2. Run: vercel
  3. Follow the prompts

Netlify

Configure Netlify deployment:
# netlify.toml
[build]
  command = "pnpm build"
  publish = ".output/public"
  functions = ".output/server"

[[redirects]]
  from = "/*"
  to = "/.netlify/functions/server"
  status = 200

Cloudflare Workers

Deploy to Cloudflare Workers:
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'

export default defineNitroConfig({
  preset: 'cloudflare-pages',
})
# wrangler.toml
name = "my-tanstack-app"
compatibility_date = "2024-01-01"

[build]
command = "pnpm build"

[site]
bucket = ".output/public"
Deploy with Wrangler:
pnpm add -D wrangler
pnpm wrangler pages deploy .output/public

AWS Lambda

Deploy to AWS Lambda:
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'

export default defineNitroConfig({
  preset: 'aws-lambda',
})
Deploy with AWS CDK:
import * as cdk from 'aws-cdk-lib'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'

const fn = new lambda.Function(this, 'TanStackStartFunction', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('.output/server'),
})

const api = new apigateway.LambdaRestApi(this, 'TanStackStartAPI', {
  handler: fn,
})

Docker

Deploy with Docker:
# Dockerfile
FROM node:20-alpine AS base

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

# Build stage
FROM base AS builder
WORKDIR /app

# Copy package files
COPY package.json pnpm-lock.yaml ./

# Install dependencies
RUN pnpm install --frozen-lockfile

# Copy source
COPY . .

# Build application
RUN pnpm build

# Production stage
FROM base AS runner
WORKDIR /app

# Copy built application
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./

# Set environment
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# Start server
CMD ["node", ".output/server/index.mjs"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    depends_on:
      - db
  
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

Self-hosted / VPS

Deploy to a VPS with Node.js:
# On your server

# 1. Clone and build
git clone https://github.com/your-repo/your-app.git
cd your-app
pnpm install
pnpm build

# 2. Install PM2 for process management
pnpm add -g pm2

# 3. Start with PM2
pm2 start .output/server/index.mjs --name tanstack-app

# 4. Configure Nginx as reverse proxy
sudo nano /etc/nginx/sites-available/tanstack-app
# /etc/nginx/sites-available/tanstack-app
server {
  listen 80;
  server_name example.com;

  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
  
  # Serve static assets directly
  location /_build/ {
    alias /path/to/app/.output/public/_build/;
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
}
# Enable site and restart Nginx
sudo ln -s /etc/nginx/sites-available/tanstack-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

# Configure PM2 to start on boot
pm2 startup
pm2 save

Environment Variables

Manage environment variables securely:

Development

# .env.local (not committed)
DATABASE_URL=postgresql://localhost:5432/dev
API_SECRET=dev-secret-key

Production

# Set via platform UI or CLI

# Vercel
vercel env add DATABASE_URL production

# Netlify
netlify env:set DATABASE_URL "postgresql://..."

# Cloudflare
wrangler secret put DATABASE_URL

# AWS
aws lambda update-function-configuration \
  --function-name my-function \
  --environment Variables={DATABASE_URL=postgresql://...}

Accessing in Code

import { createServerFn } from '@tanstack/react-start'

// Environment variables are only available on the server
const getConfig = createServerFn({ method: 'GET' })
  .handler(() => {
    return {
      // ✅ Safe - server only
      apiUrl: process.env.API_URL,
      // ❌ Never expose secrets!
      // apiKey: process.env.API_KEY,
    }
  })

Asset Optimization

CDN Configuration

Serve static assets from a CDN:
// src/entry-server.tsx
import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server'

export default createStartHandler({
  handler: defaultStreamHandler,
  transformAssetUrls: 'https://cdn.example.com',
})
This transforms all asset URLs:
<!-- Before -->
<script type="module" src="/assets/index-abc123.js"></script>
<link rel="stylesheet" href="/assets/index-def456.css" />

<!-- After -->
<script type="module" src="https://cdn.example.com/assets/index-abc123.js"></script>
<link rel="stylesheet" href="https://cdn.example.com/assets/index-def456.css" />
Reference: packages/start-server-core/src/createStartHandler.ts:59-111

Dynamic CDN URLs

Use different CDN URLs per request:
import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server'
import { getRequest } from '@tanstack/react-start/server'

export default createStartHandler({
  handler: defaultStreamHandler,
  transformAssetUrls: {
    transform: ({ url, type }) => {
      // Get region from request
      const request = getRequest()
      const region = request.headers.get('x-region') || 'us'
      
      // Use region-specific CDN
      return `https://cdn-${region}.example.com${url}`
    },
    cache: false, // Transform per-request
  },
})
Reference: packages/start-server-core/src/createStartHandler.ts:83-95

Cache Headers

Set aggressive caching for static assets:
# Nginx configuration
location /assets/ {
  expires 1y;
  add_header Cache-Control "public, immutable";
  # Enable Brotli compression
  brotli on;
  brotli_types text/css application/javascript application/json;
}
// Or in server code
import { getResponse } from '@tanstack/react-start/server'

const loader = async () => {
  const response = getResponse()
  response.headers.set(
    'Cache-Control',
    'public, max-age=31536000, immutable'
  )
  return { data }
}

Performance Optimization

1. Enable Compression

// nitro.config.ts
export default defineNitroConfig({
  compressPublicAssets: true,
})

2. Database Connection Pooling

// lib/db.ts
import { Pool } from 'pg'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
})

export const db = {
  query: (text: string, params?: any[]) => pool.query(text, params),
}

3. Response Caching

import { createServerFn } from '@tanstack/react-start'
import { getResponse } from '@tanstack/react-start/server'

const getPublicData = createServerFn({ method: 'GET' })
  .handler(async () => {
    const response = getResponse()
    
    // Cache for 1 hour, revalidate in background
    response.headers.set(
      'Cache-Control',
      'public, max-age=3600, stale-while-revalidate=86400'
    )
    
    const data = await db.query('SELECT * FROM public_data')
    return data
  })

4. Code Splitting

Lazy load routes and components:
import { lazy } from 'react'
import { createFileRoute } from '@tanstack/react-router'

// Heavy component - lazy loaded
const HeavyChart = lazy(() => import('./HeavyChart'))

export const Route = createFileRoute('/analytics')({
  component: () => (
    <Suspense fallback={<div>Loading chart...</div>}>
      <HeavyChart />
    </Suspense>
  ),
})

5. Bundle Analysis

# Analyze bundle size
pnpm add -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    // ... other plugins
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
})

Monitoring and Logging

Error Tracking

// src/lib/error-tracking.ts
import * as Sentry from '@sentry/node'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
})

export { Sentry }
// Use in server functions
import { createServerFn } from '@tanstack/react-start'
import { Sentry } from './lib/error-tracking'

const riskyOperation = createServerFn({ method: 'POST' })
  .handler(async ({ data }) => {
    try {
      return await performOperation(data)
    } catch (error) {
      Sentry.captureException(error)
      throw error
    }
  })

Performance Monitoring

// Middleware for request timing
import { createMiddleware } from '@tanstack/react-start'

export const timingMiddleware = createMiddleware()
  .server(async ({ request, next }) => {
    const start = Date.now()
    const result = await next()
    const duration = Date.now() - start
    
    console.log(`${request.method} ${request.url} - ${duration}ms`)
    
    // Send to analytics service
    if (process.env.NODE_ENV === 'production') {
      analytics.track('request', {
        path: new URL(request.url).pathname,
        method: request.method,
        duration,
      })
    }
    
    return result
  })

Security

1. Security Headers

// Middleware for security headers
import { createMiddleware } from '@tanstack/react-start'

export const securityMiddleware = createMiddleware()
  .server(async ({ next }) => {
    const result = await next()
    
    // Add security headers
    const response = getResponse()
    response.headers.set('X-Content-Type-Options', 'nosniff')
    response.headers.set('X-Frame-Options', 'DENY')
    response.headers.set('X-XSS-Protection', '1; mode=block')
    response.headers.set(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains'
    )
    response.headers.set(
      'Content-Security-Policy',
      "default-src 'self'; script-src 'self' 'unsafe-inline'"
    )
    
    return result
  })

2. Rate Limiting

import { createMiddleware } from '@tanstack/react-start'
import rateLimit from 'express-rate-limit'

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per window
})

export const rateLimitMiddleware = createMiddleware()
  .server(async ({ request, next }) => {
    const ip = request.headers.get('x-forwarded-for') || 'unknown'
    
    // Check rate limit
    const allowed = await checkRateLimit(ip)
    if (!allowed) {
      throw new Error('Too many requests')
    }
    
    return next()
  })

3. CORS Configuration

import { createMiddleware } from '@tanstack/react-start'

export const corsMiddleware = createMiddleware()
  .server(async ({ request, next }) => {
    const result = await next()
    const response = getResponse()
    
    const origin = request.headers.get('origin')
    const allowedOrigins = [
      'https://example.com',
      'https://www.example.com',
    ]
    
    if (origin && allowedOrigins.includes(origin)) {
      response.headers.set('Access-Control-Allow-Origin', origin)
      response.headers.set('Access-Control-Allow-Credentials', 'true')
      response.headers.set(
        'Access-Control-Allow-Methods',
        'GET, POST, PUT, DELETE'
      )
    }
    
    return result
  })

Health Checks

// src/routes/api/health.ts
import { createAPIFileRoute } from '@tanstack/react-start'

export const Route = createAPIFileRoute('/api/health')({
  GET: async () => {
    // Check database
    const dbHealthy = await checkDatabase()
    
    // Check external services
    const servicesHealthy = await checkExternalServices()
    
    const healthy = dbHealthy && servicesHealthy
    
    return new Response(
      JSON.stringify({
        status: healthy ? 'ok' : 'error',
        timestamp: new Date().toISOString(),
        checks: {
          database: dbHealthy,
          services: servicesHealthy,
        },
      }),
      {
        status: healthy ? 200 : 503,
        headers: { 'Content-Type': 'application/json' },
      }
    )
  },
})

async function checkDatabase(): Promise<boolean> {
  try {
    await db.query('SELECT 1')
    return true
  } catch {
    return false
  }
}

async function checkExternalServices(): Promise<boolean> {
  try {
    const response = await fetch('https://api.example.com/health')
    return response.ok
  } catch {
    return false
  }
}

Rollback Strategy

Implement safe deployments:
# Deploy with git tags
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

# If issues arise, rollback
git revert HEAD
git push origin main

# Or use platform-specific rollback
vercel rollback

Next Steps

Build docs developers (and LLMs) love