DEV Community

Natalia
Natalia

Posted on

Notes on Configuring TanStack Start for Cloudflare Workers

I like frameworks that make the boring parts stay boring.

For an AI image website like Flux 2, the frontend is only one slice of the work. There are landing pages, account flows, uploads, generation history, admin screens, and a lot of small server-side decisions around performance and reliability. That is why TanStack Start on Cloudflare Workers is interesting: Vite-based development, full-stack routing, server functions, static assets, and an edge runtime can all fit into one deployment story.

The part I care about most is not the headline feature list. It is the config.

If the config is clear, the team knows what gets deployed, where it runs, and which runtime assumptions the app is making. If the config is messy, every release feels a little suspicious.

This is the short version of how I would approach a TanStack Start + Cloudflare setup.

Start with the Cloudflare target in mind

TanStack Start can run on different hosting targets, but Cloudflare Workers has a specific shape. The official Cloudflare TanStack Start guide shows two useful paths:

  • create a new app already configured for Cloudflare
  • adapt an existing TanStack Start app by adding Wrangler and the Cloudflare Vite plugin

For a new project, I would rather start with the Cloudflare path than retrofit it later:

pnpm create cloudflare@latest my-app --framework=tanstack-start
Enter fullscreen mode Exit fullscreen mode

For an existing project, the main idea is simple: TanStack Start still uses Vite, but the Cloudflare plugin needs to participate in the server-side build.

The Vite config is the first file I check

The Vite config tells me whether the project is actually being built for the runtime I expect.

A Cloudflare-oriented setup usually looks like this:

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

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    tanstackStart(),
    react(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

The important part is not memorizing every line. The important part is that the SSR environment is intentionally wired through Cloudflare. That keeps the server side of the app close to the Worker runtime instead of accidentally drifting toward a generic Node.js assumption.

That matters when the app grows. Server functions, route loaders, auth checks, media metadata, and dashboard requests are all easier to reason about when the runtime is explicit.

Wrangler config should be small at first

The next file I look at is wrangler.jsonc.

I prefer starting small:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "example-tanstack-start",
  "compatibility_date": "2026-06-04",
  "compatibility_flags": ["nodejs_compat"],
  "main": "@tanstack/react-start/server-entry",
  "observability": {
    "enabled": true
  }
}
Enter fullscreen mode Exit fullscreen mode

This does a few useful things.

main points Wrangler at the TanStack Start server entry. compatibility_date makes the Workers runtime version explicit. nodejs_compat gives the app a more practical compatibility baseline for packages that expect some Node APIs. observability makes the first round of debugging less blind.

I do not like stuffing this file with every future binding on day one. The config should grow when the product needs it.

Add scripts that explain the workflow

The package.json scripts should make the common path obvious:

{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "deploy": "pnpm build && wrangler deploy",
    "cf-typegen": "wrangler types"
  }
}
Enter fullscreen mode Exit fullscreen mode

I like this because it separates concerns:

  • dev is for local development
  • build proves the app can compile
  • preview lets me inspect the built result
  • deploy keeps the build step attached to release
  • cf-typegen keeps Worker bindings typed

The last one is easy to skip, but it is useful once bindings enter the project. A typed env is much better than guessing whether a binding name is available in a server function.

Treat bindings as product decisions

For a real AI image product, Cloudflare bindings can become important quickly.

You might use R2 for generated media, D1 for lightweight relational data, KV for low-risk cache data, Queues for background jobs, or a service binding for splitting a larger backend into smaller Workers.

But I try not to add bindings just because the platform supports them. I add them when a workflow needs a clear ownership boundary.

For example, an upload or generated asset flow may justify an R2 binding. A scheduled cleanup job may justify a custom entrypoint. A separate auth or billing service may justify a service binding. The config should tell that story plainly.

Inside TanStack Start server code, Cloudflare bindings are accessed through the Worker environment. That is a good fit for server functions because browser code does not need to know about storage buckets, queues, or internal services.

Be careful with prerendering

TanStack Start can prerender routes, and Cloudflare can serve static assets efficiently. That is useful for pages that do not depend on per-user state.

I would consider prerendering for:

  • marketing pages
  • documentation-style pages
  • static comparison pages
  • content that changes on a predictable schedule

I would not use it blindly for account pages, generation history, payment status, or anything that depends on a signed-in user.

The trap is not prerendering itself. The trap is forgetting when data is read. Build-time data and request-time data are different. Once that distinction is clear, prerendering becomes a useful tool instead of a source of confusing stale pages.

Keep environments boring

I want staging and production to be boringly explicit.

That usually means separate Worker names and routes, with the same general structure:

{
  "env": {
    "staging": {
      "name": "example-tanstack-start-staging"
    },
    "production": {
      "name": "example-tanstack-start-production"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then commands become hard to misread:

pnpm build
wrangler deploy --env staging --dry-run
wrangler deploy --env staging
wrangler tail --env staging

wrangler deploy --env production --dry-run
wrangler deploy --env production
wrangler tail --env production
Enter fullscreen mode Exit fullscreen mode

The tail step is not glamorous, but it is one of the fastest ways to notice obvious runtime mistakes after a release.

The config is part of the product

For a TanStack Start app on Cloudflare, I think of configuration as product infrastructure, not boilerplate.

The Vite config says how the app is built. The Wrangler config says how it runs. The scripts say how developers interact with it. Bindings say which Cloudflare resources the app depends on. Environment sections say where a release is going.

That is why I prefer a small, readable setup over a clever one.

For an AI image website, the product can be complex enough already. The deployment path should be easy to inspect: TanStack Start for the app shape, Cloudflare Workers for the runtime, Wrangler for the release workflow, and only the bindings the product actually needs.

Top comments (0)