Cloudflare WorkersDeploymentTanStack StartDevOps

TanStack Start Deployment: We Deploy to Edge in 3 Minutes (Real Setup)

Real deployment pipeline. We deploy TanStack Start to Cloudflare Workers in 3 minutes. CI/CD setup, preview deployments, and rollback strategy.

Sam Rivera
Sam Rivera
May 29, 202611 min read
Also available in:Deutsch · 中文

TL;DR: Deploying a TanStack Start application to Cloudflare Workers requires careful configuration of wrangler.jsonc, environment variable management across dev and production environments, D1 database setup with automated migrations, R2 object storage integration, and a solid CI/CD pipeline. This guide walks through every step of the process — from initial scaffolding to production monitoring — so you can ship with confidence on Cloudflare's edge network.


Introduction: Why Cloudflare Workers for TanStack Start

TanStack Start is a full-stack React framework built on Vinxi that brings server-side rendering, streaming, and static generation to the TanStack ecosystem. When it comes to deployment, Cloudflare Workers offer a uniquely compelling runtime for Start applications.

Unlike traditional Node.js servers that require cold starts, region pinning, and manual scaling, Cloudflare Workers run on V8 isolates across 330+ locations worldwide. This means your TanStack Start app can serve initial HTML, stream content updates, and handle API requests — all from the edge, close to every user.

But with this architectural shift comes a new set of configuration challenges. The Workers runtime doesn't support Node.js APIs like fs, net, or child_process. Your build pipeline, environment variables, and database connections all need to be adapted. This guide covers the production-ready setup we use to run TanStack Start on Cloudflare Workers at scale.


wrangler.jsonc Deep Configuration

The wrangler.jsonc file is the single source of truth for your Cloudflare deployment. Here is the production configuration we use, annotated with explanations:

jsonc
{
  "$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json",
  "name": "tanstack-ship",
  "main": "./dist/worker.js",
  "compatibility_date": "2026-05-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": "./dist/public",
    "binding": "ASSETS",
    "html_handling": "auto",
    "not_found_handling": "404"
  },
  "workers_dev": false,
  "route": {
    "pattern": "tanstack-ship.com",
    "custom_domain": true,
    "zone_id": "YOUR_ZONE_ID"
  },
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "tanstack-ship-db",
      "database_id": "YOUR_D1_DATABASE_ID",
      "migrations_dir": "./migrations"
    }
  ],
  "r2_buckets": [
    {
      "binding": "R2",
      "bucket_name": "tanstack-ship-assets"
    }
  ],
  "vars": {
    "APP_VERSION": "1.0.0",
    "SENTRY_DSN": ""
  },
  "env": {
    "production": {
      "vars": {
        "SENTRY_DSN": "https://your-sentry-dsn@o123.ingest.sentry.io/456"
      },
      "routes": [
        {
          "pattern": "tanstack-ship.com",
          "custom_domain": true
        }
      ]
    },
    "preview": {
      "workers_dev": true,
      "vars": {
        "SENTRY_DSN": ""
      }
    }
  },
  "observability": {
    "enabled": true,
    "head_sampling_rate": 0.1
  }
}

Key Fields Explained

  • compatibility_flags: The nodejs_compat flag enables a subset of Node.js APIs (e.g., Buffer, process.env). Many npm packages depend on these, so keep this flag enabled.
  • assets: The html_handling: "auto" instructs Workers to serve index.html for directory requests — essential for client-side routing fallback. The not_found_handling: "404" ensures unknown paths return a proper 404.
  • env blocks: Define environment-specific overrides. The preview environment uses workers_dev: true for *.workers.dev subdomain, while production binds to a custom domain.

Environment Variables: Dev vs. Production

Local Development

Create a .dev.vars file in your project root:

# .dev.vars — loaded by wrangler dev
SENTRY_DSN=
STRIPE_SECRET_KEY=your-stripe-secret-key
RESEND_API_KEY=your-resend-api-key
PUBLIC_SITE_URL=http://localhost:5173

Cloudflare's wrangler dev command automatically reads .dev.vars and injects them into the Worker runtime.

Production Secrets

For production secrets, use wrangler secret put to keep sensitive values out of your config files:

bash
npx wrangler secret put SENTRY_DSN
npx wrangler secret put STRIPE_SECRET_KEY

Secrets are encrypted at rest and injected at runtime on the edge. They are not accessible in the vars section of wrangler.jsonc — a common security mistake.

Type-Safe Access Pattern

Centralize all environment variable access in a single module with Zod validation for runtime safety:

typescript
// src/env.ts
import { z } from "zod";

const envSchema = z.object({
  SENTRY_DSN: z.string().optional(),
  STRIPE_SECRET_KEY: z.string().min(1),
  RESEND_API_KEY: z.string().min(1),
  PUBLIC_SITE_URL: z.string().url().default("http://localhost:5173"),
  DB: z.any(),    // D1 binding
  R2: z.any(),    // R2 binding
  ASSETS: z.any(), // Assets binding
});

export type Env = z.infer<typeof envSchema>;

export function parseEnv(env: Record<string, unknown>): Env {
  const result = envSchema.safeParse(env);
  if (!result.success) {
    console.error("Invalid environment variables:", result.error.flatten());
    throw new Error("Environment validation failed");
  }
  return result.data;
}

This pattern gives you runtime type safety and catches misconfigured environments early in CI.


D1 Database Setup and Migrations

1. Create the Database

bash
npx wrangler d1 create tanstack-ship-db

Copy the returned database_id into your wrangler.jsonc.

2. Write Migrations

Place SQL migration files in a migrations/ directory:

sql
-- migrations/0001_users.sql
CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_users_email ON users(email);

3. Apply Migrations

Locally:

bash
npx wrangler d1 migrations apply tanstack-ship-db --local

In production:

bash
npx wrangler d1 migrations apply tanstack-ship-db --remote

4. Automated Migrations in CI

yaml
# .github/workflows/deploy.yml (excerpt)
- name: Apply D1 Migrations
  run: npx wrangler d1 migrations apply tanstack-ship-db --remote
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Run migrations before deploying the new Worker version for zero-downtime deployments.


R2 Object Storage Integration

Add an R2 binding in wrangler.jsonc, then configure the client for file uploads and retrieval:

typescript
// src/lib/r2-client.ts
import type { R2Bucket } from "@cloudflare/workers-types";

let bucket: R2Bucket;

export function initR2(r2: R2Bucket) {
  bucket = r2;
}

export async function uploadFile(
  key: string,
  body: ReadableStream | ArrayBuffer,
  contentType: string
) {
  return bucket.put(key, body, {
    httpMetadata: { contentType },
    customMetadata: { uploadedAt: new Date().toISOString() },
  });
}

export async function getFile(key: string) {
  const object = await bucket.get(key);
  if (!object) return null;
  return object;
}

Production Optimizations

Caching Headers

typescript
function setCachingHeaders(response: Response, pathname: string): Response {
  const headers = new Headers(response.headers);

  // Static assets: long-term cache
  if (/\.(js|css|png|jpg|webp|svg|woff2?)$/i.test(pathname)) {
    headers.set("Cache-Control", "public, max-age=31536000, immutable");
  }
  // HTML pages: short CDN cache, no browser cache
  else if (pathname.endsWith(".html") || !pathname.includes(".")) {
    headers.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=600");
  }

  return new Response(response.body, {
    status: response.status,
    headers,
  });
}

Enable Streaming

jsonc
// wrangler.jsonc
{
  "compatibility_flags": ["nodejs_compat"],
  "streaming": true
}

Streaming allows Suspense boundaries to send content progressively, improving perceived performance.


CI/CD Pipeline Setup

yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: npm ci
      - run: npm run typecheck
      - run: npm run lint
      - run: npm run build
        env:
          PUBLIC_SITE_URL: ${{ vars.PUBLIC_SITE_URL }}
      - name: Apply D1 Migrations
        run: npx wrangler d1 migrations apply tanstack-ship-db --remote
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
      - name: Deploy to Cloudflare Workers
        run: npx wrangler deploy --env production
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Common Deployment Issues and Solutions

  1. "Module not found" for Node.js built-ins: Enable nodejs_compat flag. If the module still fails, find a Workers-compatible alternative.

  2. D1 migration fails with "Table already exists": Always use CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS.

  3. Client-side routing returns 404 on refresh: Ensure html_handling: "auto" is set in wrangler.jsonc.

  4. Environment variables are undefined at runtime: Move secrets to wrangler secret put and only use vars for non-sensitive configuration.

  5. Large bundle exceeds 1 MB limit: Ensure your Vite build tree-shakes unused code. Use dynamic import() for heavy dependencies.


Conclusion

Deploying TanStack Start on Cloudflare Workers is one of the most performant and cost-effective ways to ship a full-stack React application. The edge runtime gives you global distribution, instant cold starts, and a generous free tier.

The key takeaways are:

  • Validate your environment variables with Zod at the edge before any logic runs.
  • Run migrations before deployments, not after.
  • Set caching headers aggressively but thoughtfully — HTML gets s-maxage, assets get immutable.
  • Enable observability early — you will need those traces when something goes wrong.

For a hands-on walkthrough of building a complete application, see the TanStack Start End-to-End Tutorial. To take your D1 database to production, read Edge Database: Cloudflare D1 in Production. For official references, consult the Wrangler CLI, Cloudflare D1, and R2 documentation.