How to Deploy TanStack Start to Cloudflare Workers (The Right Way)
Deploying a TanStack Start app to Cloudflare Workers takes about 15 minutes. I expected pain — I'd been through the Next.js deployment wars, Vercel lock-in, mysterious edge runtime limitations, 3am 500s. What I found instead was a clean setup, blazing-fast edge delivery globally, and a free tier generous enough to carry you from zero to paying customers without spending a cent.
Here's exactly how to do it.
Why Cloudflare Workers for TanStack Start?
TanStack Start runs on Vite and Nitro under the hood, which means it supports multiple deployment targets out of the box — Node.js, Bun, Vercel, and Cloudflare Workers. Workers is my first choice for new SaaS projects because:
- Zero cold starts — Workers run on V8 isolates, not containers. Requests hit instantly.
- Global edge network — 300+ locations. Sub-10ms TTFB for most of your users.
- Generous free tier — 100,000 requests/day free. That's plenty for an early-stage product.
- No platform lock-in — You control your infra. No surprise Vercel bills when you go viral.
If you're building a SaaS and want fast server functions without paying Vercel for the privilege, Workers is the answer. And since TanStack Start is built for full-stack TypeScript, the Workers runtime fits perfectly.
The Deployment Architecture
Here's how TanStack Start maps onto Cloudflare's infrastructure:
graph TD
A[User Request] --> B[Cloudflare Edge Network]
B --> C[Workers V8 Isolate]
C --> D[TanStack Start SSR]
D --> E[Server Functions]
E --> F[Drizzle ORM + D1 Database]
D --> G[Static Assets via CDN]
C --> H[Response - sub-10ms]
style B fill:#F6821F,color:#fff
style C fill:#F6821F,color:#fff
style H fill:#2ecc71,color:#fff
Your app builds once, deploys to every edge node, and SSR happens close to your user. Server functions hit your database from the same edge node — no round trips to a central server.
Step 1: Install the Cloudflare Vite Plugin
Cloudflare's official Vite plugin handles the Workers adapter for TanStack Start:
bun add -D @cloudflare/vite-plugin wrangler
Step 2: Configure Your App
Update app.config.ts to use the Cloudflare preset:
import { defineConfig } from '@tanstack/start/config'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
server: {
preset: 'cloudflare-module',
},
vite: {
plugins: [
cloudflare(),
],
},
})
Step 3: Create wrangler.toml
The wrangler.toml tells Cloudflare what your Worker is and how to run it:
name = "my-saas-app"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]
main = ".output/server/index.mjs"
[site]
bucket = ".output/public"
[[d1_databases]]
binding = "DB"
database_name = "my-saas-db"
database_id = "your-d1-database-id-here"
The nodejs_compat flag is non-negotiable. TanStack Start's server functions use Node.js APIs — without this flag, you'll hit cryptic runtime errors. Enable it.
Step 4: Wire Up Drizzle ORM with D1
If you're using Drizzle ORM (the clear winner over Prisma for this stack), Cloudflare D1 is the natural database choice. D1 is SQLite-compatible and runs at the edge alongside your Worker.
Create a database utility that accepts the D1 binding from the Worker environment:
// src/db/index.ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'
export function getDb(env: Env) {
return drizzle(env.DB, { schema })
}
Use it inside your server functions:
import { createServerFn } from '@tanstack/start'
import { getDb } from '~/db'
export const getCurrentUser = createServerFn('GET', async (_, ctx) => {
const db = getDb(ctx.env)
const user = await db.query.users.findFirst({
where: (u, { eq }) => eq(u.id, ctx.session.userId),
})
return user
})
This keeps your data access clean, typed, and co-located with your server logic — no separate API layer needed.
Step 5: Set Production Secrets
Never put credentials in wrangler.toml. Use Wrangler's secrets management:
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put PLUNK_API_KEY
wrangler secret put DATABASE_URL
Secrets are encrypted at rest, scoped to your Worker, and available as process.env.VARIABLE_NAME at runtime (thanks to the nodejs_compat flag).
Step 6: Build and Deploy
# Build the app
bun run build
# Preview locally with Wrangler
bunx wrangler dev
# Deploy to production
bunx wrangler deploy
First deploy takes about 30 seconds. Subsequent deploys are faster. Your app is now running on Cloudflare's global edge network.
What About Bun in Production?
BetterStarter uses Bun — does that conflict with Workers? Not at all, and this is important to understand.
Cloudflare Workers doesn't run Bun at runtime. It uses V8 isolates. What Bun gives you is a dramatically faster local development and build experience — bun install in seconds, bun run dev that starts instantly, bun run build that's 2–3x faster than Node.js equivalents.
The build output is standard JavaScript that Workers runs perfectly. You get Bun's speed during development and Workers' edge performance in production — best of both worlds.
If you want true Bun runtime in production (for running actual Bun APIs), look at Fly.io or Railway. But for edge-deployed TanStack Start SaaS, Bun locally + Workers in production is the stack I use and recommend.
A Note on Local Development
Set up a local D1 database for development:
bunx wrangler d1 create my-saas-db
bunx wrangler d1 execute my-saas-db --local --file=./drizzle/migrations/0000_initial.sql
Then run locally with Workers emulation:
bunx wrangler dev --local
This spins up a local Workers environment with D1 emulation. Your code runs identically to production — no surprises on deploy.
CI/CD with GitHub Actions
For automatic deploys on push to main:
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Store your CLOUDFLARE_API_TOKEN in GitHub secrets and every push to main deploys automatically. Zero downtime, rollback via Wrangler in 10 seconds if something goes wrong.
FAQ
Does TanStack Start work on the Cloudflare Workers free tier? Yes. 100,000 requests/day free is plenty for early SaaS. You'll only hit paid tiers when you're making real money — a good problem to have.
Can Better-Auth run on Cloudflare Workers?
Yes. Better-Auth works in edge environments with nodejs_compat enabled. I use it in production on Workers with session storage via KV — it's solid.
Cloudflare Pages vs Workers for TanStack Start — which is right? Workers. Pages is optimized for static sites. TanStack Start is server-rendered with server functions — you need the full Workers runtime for SSR and API calls to work correctly.
Do I need a paid Cloudflare plan for D1? No. D1 is free up to 5GB storage and 5 million rows read/day. More than enough for a new SaaS. Paid plans ($5/month) unlock higher limits when you need them.
Can I run database migrations on Workers?
Not directly — Workers doesn't have a persistent file system for migration state. Run migrations during your CI/CD pipeline with wrangler d1 execute --remote before deploying the Worker.
This exact setup — TanStack Start, Drizzle ORM, Better-Auth, and Cloudflare-ready deployment config — is what ships with BetterStarter. I've done the wiring so you don't have to: auth, Stripe, email with Plunk, and a production-ready Workers deployment config included for $99 one-time. If you want to skip from idea to deployed product in a day, that's what it's for.