This website runs on TanStack Start and Cloudflare Workers, with a D1 database behind the blog. I recently moved it from a static build. In this post I share the exact TanStack Start Cloudflare Workers setup I use, plus the problems I hit, so you can skip them.

The result is a server-rendered React site with an admin panel, a blog and a contact form. It costs nothing on the free plan. Also, pages load from the edge, close to every visitor.

Why TanStack Start on Cloudflare Workers?

TanStack Start is a full-stack React framework built on TanStack Router and Vite. It renders pages on the server, so search engines see real HTML. In addition, server functions let you call backend code like a normal function, with full type safety.

Cloudflare Workers, on the other hand, run your code in more than 300 cities. There are no servers to manage and no cold starts worth noticing. In addition, the free plan includes 100,000 requests per day. For a personal site or a small business, that is plenty.

Together they give you SSR, an API and a database in one deploy. For SEO, that combination is hard to beat.

Step 1: Add the Cloudflare Vite plugin to TanStack Start

Cloudflare maintains an official Vite plugin. It runs your app inside the real Workers runtime during development. As a result, local behaviour matches production closely.

npm i -D @cloudflare/vite-plugin wrangler

Then register it in vite.config.ts before the TanStack Start plugin:

import { cloudflare } from "@cloudflare/vite-plugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    tanstackStart(),
    viteReact(),
  ],
});

Step 2: Configure wrangler.jsonc

Next, create wrangler.jsonc. The main field points at the server entry that TanStack Start ships:

{
  "name": "my-site",
  "compatibility_date": "2026-11-14",
  "compatibility_flags": ["nodejs_compat"],
  "main": "@tanstack/react-start/server-entry",
  "d1_databases": [
    { "binding": "DB", "database_name": "my-blog", "database_id": "…" }
  ]
}

The nodejs_compat flag matters. It enables Node APIs and fills process.env with your variables and secrets.

Step 3: Use D1 as the database

D1 is Cloudflare's SQLite database. Create it with one command, then keep your schema in numbered migration files:

npx wrangler d1 create my-blog
npx wrangler d1 migrations apply my-blog --local
npx wrangler d1 migrations apply my-blog --remote

Inside server functions, read the binding from cloudflare:workers. Then write plain SQL with bound parameters:

import { env } from "cloudflare:workers";

const post = await env.DB
  .prepare("SELECT * FROM posts WHERE slug = ?")
  .bind(slug)
  .first();

Moving from MongoDB took less time than I expected. In fact, most queries became shorter. Also, db.batch() runs several statements in one transaction, which keeps writes safe.

Gotcha 1: bcrypt does not run on Workers

My admin login used bcrypt. However, native modules do not run on Workers. Pure JavaScript bcrypt works, but it is slow and can hit the CPU limit.

So I switched to PBKDF2-SHA256 with the built-in Web Crypto API. It runs natively, so it is fast. I store the iteration count with each hash, so I can raise it later without breaking old passwords.

Gotcha 2: R2 needs billing turned on

I planned to store uploaded images in R2. Then the deploy failed with error 10042. R2 has a free tier, but you must still enable it with a payment method on file.

Instead, I store images in D1. Each file is split into 1 MB chunks, because a D1 row can hold at most 2 MB. The browser also resizes uploads to WebP at four widths before sending them. Finally, the Worker caches each image at the edge, so D1 is rarely read twice.

Gotcha 3: Secrets and environment variables

Never commit secrets. Instead, locally put them in .dev.vars and add that file to .gitignore. In production, set them once with Wrangler:

npx wrangler secret put SESSION_SECRET

Public values, such as a site environment flag, belong in the vars block of wrangler.jsonc. Both show up on process.env at runtime.

Local development tips

Run npm run dev and the Cloudflare plugin starts a local copy of D1 inside .wrangler/state. Because it is a real SQLite file, you can reset it at any time by deleting that folder and applying the migrations again.

Also, keep a small script that creates your first admin user. Mine hashes the password with the same PBKDF2 code the Worker uses, then runs wrangler d1 execute with either --local or --remote. As a result, setting up a new machine takes about a minute.

Finally, run tsc --noEmit and a production build before every push. The Workers runtime is strict about Node APIs, so a build that passes locally is your best early warning.

Step 4: Deploy TanStack Start from GitHub

You can deploy from your laptop with npm run build and npx wrangler deploy. Even better, connect the GitHub repository in the dashboard under Workers & Pages. After that, every push to main builds and deploys on its own.

Make sure the Worker name in the dashboard matches the name in wrangler.jsonc. Otherwise the build creates a second Worker. Then add your domain under Settings → Domains & Routes.

Results

  • Server-rendered HTML for every page, with JSON-LD and a dynamic sitemap.
  • Typical response times of 50 to 150 ms.
  • A blog admin, contact inbox and image uploads, all on the free plan.
  • Automatic deploys on every push.

Is TanStack Start on Cloudflare worth it?

Running TanStack Start on Cloudflare Workers gives you a fast, SEO-friendly React app with almost no hosting cost. Just plan for the Workers runtime: use Web Crypto, keep secrets out of Git and pick storage that fits the free plan.

Want a site or web app like this for your business? I design, build and deploy them end to end. Read about what I do or send me a message.