Full-stack template with TanStack Start, TanStack Query, Shadcn UI, and Cloudflare Workers (D1, R2, KV) — with GitHub Actions CI/CD included.
- Posts CRUD (
/posts) — D1 serverless SQLite with a complete REST API example - File Storage (
/files) — R2 object storage with drag & drop upload, download, and delete - Settings & Cache (
/settings) — KV namespace for key-value settings and a TTL cache demo - Modern UI — Shadcn UI components with Tailwind CSS v4, dark mode included
- CI/CD — GitHub Actions workflow that builds, migrates, and deploys on every push to
main
- TanStack Start — Full-stack React framework with file-based routing and server route handlers
- TanStack Query — Client-side data fetching and cache invalidation
- Shadcn UI — Accessible components built with Radix and Tailwind CSS
- Cloudflare Workers — Edge deployment with 0ms cold starts
- D1 — Serverless SQLite database at the edge
- R2 — S3-compatible object storage with no egress fees
- KV — Low-latency key-value storage
- Bun — Fast JavaScript runtime and package manager
# Install dependencies (also generates Cloudflare binding types)
bun install
# Apply database migrations locally
bun run db:migrate
# Start development server
bun run devsrc/
├── routes/
│ ├── __root.tsx # Root layout with nav
│ ├── index.tsx # Home page
│ ├── about.tsx # About page
│ ├── posts.tsx # Posts CRUD UI (D1)
│ ├── files.tsx # File management UI (R2)
│ ├── settings.tsx # KV settings + cache demo UI
│ └── api/
│ ├── posts.ts # Posts REST API (GET, POST, PUT, DELETE)
│ ├── settings.ts # Settings REST API (GET, POST, DELETE)
│ ├── cache.$.ts # Cache API with splat param
│ ├── files.list.ts # File listing endpoint
│ ├── files.upload.ts # File upload endpoint
│ └── files.$.ts # File serving/delete endpoint
├── components/
│ └── ui/ # Shadcn UI components
├── lib/utils.ts # cn() utility
├── styles/app.css # Tailwind v4 theme
├── types.ts # Shared API types (Post, FileItem, Setting)
└── utils/
├── cloudflare.ts # getDB(), getBucket(), getKV() binding helpers
└── api.ts # handleApi() error wrapper + input validation helpers
migrations/ # D1 SQL migrations
bunx wrangler d1 create my-app-db
bunx wrangler r2 bucket create my-app-bucket
bunx wrangler kv namespace create KVEach command above prints an ID — copy them into wrangler.jsonc:
Important: the
database_idis what the deployed Worker actually binds to — if it points at the wrong database you'll get 500s even though migrations (applied by binding name) succeed. Always paste the ID from the create command's output.
bun run db:migrate:prodbun run deployThe included workflow (.github/workflows/ci.yml) builds every push and PR, and on pushes to main it applies D1 migrations and deploys to Cloudflare Workers.
Add two repository secrets (Settings → Secrets and variables → Actions):
| Secret | Where to get it |
|---|---|
CLOUDFLARE_API_TOKEN |
Create an API token with the Edit Cloudflare Workers template plus D1 Edit permission |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare dashboard → Workers & Pages → Account ID (right sidebar) |
bun run db:migrate # Apply migrations locally
bun run db:migrate:prod # Apply migrations to production
bun run db:studio # Open D1 Studio for the local DB# List posts
curl https://your-worker.workers.dev/api/posts
# Create a post
curl -X POST https://your-worker.workers.dev/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "Hello World", "content": "My first post"}'
# Update a post (ID in body)
curl -X PUT https://your-worker.workers.dev/api/posts \
-H "Content-Type: application/json" \
-d '{"id": 1, "title": "Updated title"}'
# Delete a post (ID as query param)
curl -X DELETE "https://your-worker.workers.dev/api/posts?id=1"# Upload a file (10MB limit)
curl -X POST https://your-worker.workers.dev/api/files/upload \
-F "file=@/path/to/file.pdf"
# List files
curl https://your-worker.workers.dev/api/files/list# Set a value
curl -X POST https://your-worker.workers.dev/api/settings \
-H "Content-Type: application/json" \
-d '{"key": "theme", "value": "dark"}'
# Get all settings
curl https://your-worker.workers.dev/api/settingsThe demo endpoints ship without authentication: anyone who finds your deployed URL can create posts, upload files (10MB each) to your R2 bucket, and write KV entries. That's fine for trying the template out, but add auth (or delete the demo routes) before using it for anything real.
A minimal guard using a Worker secret looks like this:
// wrangler secret put API_TOKEN, then inside a handler:
import { env } from 'cloudflare:workers'
if (request.headers.get('Authorization') !== `Bearer ${(env as Env).API_TOKEN}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}Protections that are already built in:
- Uploaded files are served with
Content-Disposition: attachmentandX-Content-Type-Options: nosniffunless their content type is on an inline-safe allowlist (images, PDF, plain text, audio/video), so uploaded HTML or SVG can't run scripts on your origin. - The file API only reaches keys under the
uploads/prefix; the rest of the bucket is not exposed. - All SQL uses bound parameters, and every JSON body is validated before use (malformed input returns a 400, not a 500).
MIT
{ "d1_databases": [{ "binding": "DB", "database_name": "my-app-db", "database_id": "YOUR_DATABASE_ID", "migrations_dir": "migrations" }], "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-app-bucket" }], "kv_namespaces": [{ "binding": "KV", "id": "YOUR_KV_NAMESPACE_ID" }] }