TanStack Start ↗ is a full-stack framework for building web applications with server-side rendering, streaming, server functions, and bundling.
Create a TanStack Start application pre-configured for Cloudflare Workers:
npm
create cloudflare@latest -- my-tanstack-start-app --framework=tanstack-start
yarn
create cloudflare my-tanstack-start-app --framework=tanstack-start
pnpm
create cloudflare@latest my-tanstack-start-app --framework=tanstack-start
bun
create cloudflare@latest my-tanstack-start-app --framework=tanstack-start
Start a local development server to preview your project during development:
npm
run dev
yarn
run dev
pnpm
run dev
bun
run dev
If you have an existing TanStack Start application, configure it to run on Cloudflare Workers:
-
Install
@cloudflare/vite-pluginandwrangler:npm i @cloudflare/vite-plugin wrangler -- -Dyarn add @cloudflare/vite-plugin wrangler -Dpnpm add @cloudflare/vite-plugin wrangler -Dbun add @cloudflare/vite-plugin wrangler -D -
Add the Cloudflare plugin to your Vite configuration:
vite.config.jsjs import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), tanstackStart(), react(), ], });vite.config.tsts import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), tanstackStart(), react(), ], }); -
Add a
wrangler.jsoncconfiguration file:{ "$schema": "node_modules/wrangler/config-schema.json", "name": "<YOUR_PROJECT_NAME>", // Set this to today's date "compatibility_date": "2026-07-20", "compatibility_flags": ["nodejs_compat"], "main": "@tanstack/react-start/server-entry", "observability": { "enabled": true, }, }"$schema" = "node_modules/wrangler/config-schema.json" name = "<YOUR_PROJECT_NAME>" # Set this to today's date compatibility_date = "2026-07-20" compatibility_flags = [ "nodejs_compat" ] main = "@tanstack/react-start/server-entry" [observability] enabled = true -
Update the
scriptssection inpackage.json:package.jsonjson { "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "deploy": "npm run build && wrangler deploy", "cf-typegen": "wrangler types" } }
Deploy to a *.workers.dev subdomain or a custom domain from your machine or any CI/CD system, including Workers Builds.
npm
run deploy
yarn
run deploy
pnpm
run deploy
bun
run deploy
TanStack Start uses @tanstack/react-start/server-entry as your default entrypoint. Create a custom server entrypoint to add additional Workers handlers such as Queues and Cron Triggers. This is also where you can add additional exports such as Durable Objects and Workflows.
-
Create a custom server entrypoint file:
src/server.jsjs import handler from "@tanstack/react-start/server-entry"; // Export Durable Objects as named exports export { MyDurableObject } from "./my-durable-object"; export default { fetch: handler.fetch, // Handle Queue messages async queue(batch, env, ctx) { for (const message of batch.messages) { console.log("Processing message:", message.body); message.ack(); } }, // Handle Cron Triggers async scheduled(event, env, ctx) { console.log("Cron triggered:", event.cron); }, };src/server.tsts import handler from "@tanstack/react-start/server-entry"; // Export Durable Objects as named exports export { MyDurableObject } from "./my-durable-object"; export default { fetch: handler.fetch, // Handle Queue messages async queue(batch, env, ctx) { for (const message of batch.messages) { console.log("Processing message:", message.body); message.ack(); } }, // Handle Cron Triggers async scheduled(event, env, ctx) { console.log("Cron triggered:", event.cron); }, }; -
Update your Wrangler configuration to point to your custom entrypoint:
{ "main": "src/server.ts", }main = "src/server.ts"
Test your scheduled handler locally using the /cdn-cgi/handler/scheduled endpoint:
curl "http://localhost:3000/cdn-cgi/handler/scheduled?cron=*+*+*+*+*"Example: Using Workflows
Export a Workflow class from your custom entrypoint to run durable, multi-step tasks:
import {
WorkflowEntrypoint,
WorkflowStep,
WorkflowEvent,
} from "cloudflare:workers";
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const result = await step.do("process data", async () => {
return `Processed: ${event.payload.input}`;
});
await step.sleep("wait", "10 seconds");
await step.do("finalize", async () => {
console.log("Workflow complete:", result);
});
}
}import {
WorkflowEntrypoint,
WorkflowStep,
WorkflowEvent,
} from "cloudflare:workers";
export class MyWorkflow extends WorkflowEntrypoint<Env> {
async run(event: WorkflowEvent<{ input: string }>, step: WorkflowStep) {
const result = await step.do("process data", async () => {
return `Processed: ${event.payload.input}`;
});
await step.sleep("wait", "10 seconds");
await step.do("finalize", async () => {
console.log("Workflow complete:", result);
});
}
}Add the Workflow configuration to your Wrangler configuration:
{
"workflows": [
{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow",
},
],
}[[workflows]]
name = "my-workflow"
binding = "MY_WORKFLOW"
class_name = "MyWorkflow"Example: Using Service Bindings
Add a service binding to call another Worker's RPC methods from your TanStack Start application:
{
"services": [
{
"binding": "AUTH_SERVICE",
"service": "auth-worker",
},
],
}[[services]]
binding = "AUTH_SERVICE"
service = "auth-worker"Call the bound Worker's methods from a server function:
import { createServerFn } from "@tanstack/react-start";
import { env } from "cloudflare:workers";
const verifyUser = createServerFn()
.inputValidator((token) => token)
.handler(async ({ data: token }) => {
const result = await env.AUTH_SERVICE.verify(token);
return result;
});import { createServerFn } from "@tanstack/react-start";
import { env } from "cloudflare:workers";
const verifyUser = createServerFn()
.inputValidator((token: string) => token)
.handler(async ({ data: token }) => {
const result = await env.AUTH_SERVICE.verify(token);
return result;
});Your TanStack Start application can be fully integrated with the Cloudflare Developer Platform, in both local development and in production, by using bindings.
Access bindings by importing the env object in your server-side code:
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { env } from "cloudflare:workers";
export const Route = createFileRoute("/")({
loader: () => getData(),
component: RouteComponent,
});
const getData = createServerFn().handler(() => {
// Access bindings via env
// For example: env.MY_KV, env.MY_BUCKET, env.AI, etc.
});
function RouteComponent() {
// ...
}import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { env } from "cloudflare:workers";
export const Route = createFileRoute("/")({
loader: () => getData(),
component: RouteComponent,
});
const getData = createServerFn().handler(() => {
// Access bindings via env
// For example: env.MY_KV, env.MY_BUCKET, env.AI, etc.
});
function RouteComponent() {
// ...
}Generate TypeScript types for your bindings based on your Wrangler configuration:
npm
run cf-typegen
yarn
run cf-typegen
pnpm
run cf-typegen
bun
run cf-typegen
With bindings, your application can be fully integrated with the Cloudflare Developer Platform, giving you access to compute, storage, AI and more.
Bindings
Add an R2 bucket binding to your Wrangler configuration:
{
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "<YOUR_BUCKET_NAME>",
},
],
}[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "<YOUR_BUCKET_NAME>"Access the bucket in a server function:
import { createServerFn } from "@tanstack/react-start";
import { env } from "cloudflare:workers";
const uploadFile = createServerFn({ method: "POST" })
.validator((data) => data)
.handler(async ({ data }) => {
await env.MY_BUCKET.put(data.key, data.content);
return { success: true };
});
const getFile = createServerFn()
.validator((key) => key)
.handler(async ({ data: key }) => {
const object = await env.MY_BUCKET.get(key);
return object ? await object.text() : null;
});import { createServerFn } from "@tanstack/react-start";
import { env } from "cloudflare:workers";
const uploadFile = createServerFn({ method: "POST" })
.validator((data: { key: string; content: string }) => data)
.handler(async ({ data }) => {
await env.MY_BUCKET.put(data.key, data.content);
return { success: true };
});
const getFile = createServerFn()
.validator((key: string) => key)
.handler(async ({ data: key }) => {
const object = await env.MY_BUCKET.get(key);
return object ? await object.text() : null;
});Prerender your application to static HTML at build time and serve as static assets.
import { defineConfig } from "vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
tanstackStart({
prerender: {
enabled: true,
},
}),
react(),
],
});import { defineConfig } from "vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
tanstackStart({
prerender: {
enabled: true,
},
}),
react(),
],
});For more options, refer to TanStack Start static prerendering ↗.
To prerender with production data, use remote bindings.
In CI environments, environment variables or secrets may not be available during the build. To make them accessible:
- Set
CLOUDFLARE_INCLUDE_PROCESS_ENV=truein your CI environment and provide the required values as environment variables. - If using Workers Builds, update your build settings.