A production-ready full-stack React template running on Cloudflare Workers with D1, R2, and SSR out of the box.
- TanStack Start — React SSR framework with file-based routing, server functions, and streaming
- TanStack Router — Type-safe routing with loaders, search params, and code splitting
- Cloudflare Workers — Edge-first serverless runtime (V8 isolates, global deployment)
- Cloudflare D1 — Serverless SQLite database at the edge
- Cloudflare R2 — S3-compatible object storage with zero egress fees
- shadcn/ui — Tailwind CSS v4 components
- TypeScript — Full type safety across client and server
- Content Collections — Build-time markdown/MDX processing for blogs
Every pattern has a working demo page with code examples:
| Demo | What it covers |
|---|---|
| Execution Model | SSR vs client rendering, server functions, isomorphic functions, hydration |
| Environment Variables | VITE_ client vars, server-only vars, wrangler secrets, .dev.vars |
| Server Functions | RPC pattern, input validation, FormData, error handling, file organization |
| API Routes | GET/POST handlers, dynamic params, splat routes, when to use vs server fns |
| D1 Database | CRUD operations, parameterized queries, migrations, schema introspection |
| R2 Storage | File uploads, image gallery, object listing, metadata, serving files |
| Markdown | Static (content-collections) and dynamic rendering, TOC extraction |
| SEO | Meta tags, Open Graph, JSON-LD structured data, sitemaps, robots.txt |
| LLMO | LLM optimization, llms.txt, schema.org, AI discoverability |
| Deploy | Wrangler setup, D1/R2 creation, secrets, deployment workflow |
- Node.js 22.12+ (required by Vite 7 and Wrangler 4)
- A Cloudflare account (free tier works)
- Wrangler CLI
# Clone the template
npx degit morrisonak/claude-start-cf my-app
cd my-app
# Install dependencies
npm install
# Create Cloudflare resources
wrangler d1 create my-db
wrangler r2 bucket create my-bucket
# Update wrangler.jsonc with your database_id and bucket name
# Deploy
npm run deploynpm run deploy # Build + deploy to Cloudflare Workers
npm run build # Production build (Vite + TypeScript check)
npm run dev # Local dev server with Miniflare
npm run cf-typegen # Regenerate TypeScript types from wrangler.jsoncsrc/
├── routes/ # File-based routes (pages + API endpoints)
│ ├── __root.tsx # Root layout, global meta tags, JSON-LD
│ ├── index.tsx # Home page
│ ├── d1.tsx # D1 database demo
│ ├── r2.tsx # R2 storage demo
│ ├── llms[.]txt.ts # Machine-readable site summary
│ └── api/ # API-only routes
├── components/ # Shared React components
│ ├── SiteNav.tsx # Navigation with desktop/mobile support
│ ├── Markdown.tsx # Markdown renderer with custom link handling
│ └── ui/ # shadcn/ui components
├── lib/
│ └── env.ts # getDB(), getBucket(), getEnv() helpers
├── utils/
│ └── markdown.ts # Unified markdown rendering pipeline
└── blog/ # Markdown blog posts with YAML frontmatter
wrangler.jsonc # Cloudflare Workers config (bindings, vars)
.dev.vars # Local dev secrets (gitignored)
.env # VITE_ build-time vars (gitignored)
.env.example # Template showing required variables
import { createServerFn } from '@tanstack/react-start'
import { getDB } from '~/lib/env'
const listUsers = createServerFn({ method: 'GET' })
.handler(async () => {
const db = getDB()
const { results } = await db.prepare('SELECT * FROM users').all()
return results
})Runs on the server. Called from the client as a fetch request. Type-safe end to end.
const db = getDB()
await db.prepare('INSERT INTO items (name) VALUES (?)').bind(name).run()
const { results } = await db.prepare('SELECT * FROM items').all<Item>()const bucket = getBucket()
await bucket.put('uploads/photo.png', bytes, {
httpMetadata: { contentType: 'image/png' },
})
const obj = await bucket.get('uploads/photo.png')wrangler.jsonc vars → Non-secret config (committed)
wrangler secret put → Production secrets (encrypted)
.dev.vars → Local dev secrets (gitignored)
.env → VITE_ build-time vars only (gitignored)
Two edge-runtime issues this template already solves, worth knowing when you build on it:
- A Worker cannot
fetch()its own public hostname. Cloudflare blocks self-requests, so anything that needs to call one of your own routes server-side (like the SEO head inspector) must go through a self-referencing service binding:getEnv().SELF.fetch(url). Theservicesentry inwrangler.jsoncpoints the binding at the Worker itself. - The Workers build resolves npm packages with the
browsercondition. Packages that ship separate browser and server builds can end up running DOM-dependent code during SSR, which silently breaks server rendering (React recovers by client rendering, so pages look fine but crawlers get an empty body).vite.config.tsincludes a small plugin that forceshtml-dom-parser(used byhtml-react-parser) to its server build in the SSR environment; use the same pattern if another dependency hits this.
| Layer | Technology |
|---|---|
| Framework | TanStack Start |
| Routing | TanStack Router |
| Runtime | Cloudflare Workers |
| Database | Cloudflare D1 |
| Storage | Cloudflare R2 |
| Styling | Tailwind CSS v4 + shadcn/ui |
| Build | Vite |
| Content | content-collections |
MIT
{ "name": "my-app", "compatibility_date": "2025-09-24", "compatibility_flags": ["nodejs_compat"], "main": "@tanstack/react-start/server-entry", "vars": { "APP_ENVIRONMENT": "production" }, "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "your-database-id" }], "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }], // Lets the Worker call its own routes (Workers can't fetch their own hostname) "services": [{ "binding": "SELF", "service": "my-app" }] }