Skip to main content

tanstack-start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per project.

Source facts

Repository
jezweb/claude-skills
Last source activity
September 29, 2026 at 04:23
Detected SKILL.md language
English
Stars
1,034
Forks
106

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
tanstack-start
description
Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per project.
compatibility
claude-code-only
# TanStack Start on Cloudflare Build a complete full-stack app from nothing. Claude generates every file — no template clone, no scaffold command. Stack: TanStack Start v1 (SSR, file-based routing, server functions via Nitro) on Cloudflare Workers; React 19 + Tailwind v4 + shadcn/ui; D1 + Drizzle; better-auth (Google OAuth + email/password). ## Project File Tree ``` PROJECT_NAME/ ├── src/ │ ├── routes/ │ │ ├── __root.tsx # Root layout (HTML shell, theme, CSS import) │ │ ├── index.tsx # Landing / auth redirect │ │ ├── login.tsx # Login page │ │ ├── register.tsx # Register page │ │ ├── _authed.tsx # Auth guard layout route │ │ ├── _authed/ │ │ │ ├── dashboard.tsx # Dashboard with stat cards │ │ │ ├── items.tsx # Items list table │ │ │ ├── items.$id.tsx # Edit item │ │ │ └── items.new.tsx # Create item │ │ └── api/ │ │ └── auth/ │ │ └── $.ts # better-auth API catch-all │ ├── components/ │ │ ├── ui/ # shadcn/ui components (auto-installed) │ │ ├── app-sidebar.tsx # Navigation sidebar │ │ ├── theme-toggle.tsx # Light/dark/system toggle │ │ ├── user-nav.tsx # User dropdown menu │ │ └── stat-card.tsx # Dashboard stat card │ ├── db/ │ │ ├── schema.ts # Drizzle schema (all tables) │ │ └── index.ts # Drizzle client factory │ ├── lib/ │ │ ├── auth.server.ts # better-auth server config │ │ ├── auth.client.ts # better-auth React hooks │ │ └── utils.ts # cn() helper for shadcn/ui │ ├── server/ │ │ └── functions.ts # Server functions (CRUD, auth checks) │ ├── styles/ │ │ └── app.css # Tailwind v4 + shadcn/ui CSS variables │ ├── router.tsx # TanStack Router configuration │ ├── client.tsx # Client entry (hydrateRoot) │ ├── ssr.tsx # SSR entry │ └── routeTree.gen.ts # Auto-generated route tree (do not edit) ├── drizzle/ # Generated migrations ├── public/ # Static assets (favicon, etc.) ├── vite.config.ts ├── wrangler.jsonc ├── drizzle.config.ts ├── tsconfig.json ├── package.json ├── .dev.vars # Local env vars (NOT committed) └── .gitignore ``` ## Dependencies **Runtime:** ```json { "react": "^19.0.0", "react-dom": "^19.0.0", "@tanstack/react-router": "^1.120.0", "@tanstack/react-start": "^1.120.0", "drizzle-orm": "^0.38.0", "better-auth": "^1.2.0", "zod": "^3.24.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "tailwind-merge": "^3.0.0", "lucide-react": "^0.480.0" } ``` **Dev:** ```json { "@cloudflare/vite-plugin": "^1.0.0", "@tailwindcss/vite": "^4.0.0", "@vitejs/plugin-react": "^4.4.0", "tailwindcss": "^4.0.0", "typescript": "^5.7.0", "drizzle-kit": "^0.30.0", "wrangler": "^4.0.0", "tw-animate-css": "^1.2.0" } ``` **Scripts:** ```json { "dev": "vite", "build": "vite build", "preview": "vite preview", "deploy": "wrangler deploy", "db:generate": "drizzle-kit generate", "db:migrate:local": "wrangler d1 migrations apply PROJECT_NAME-db --local", "db:migrate:remote": "wrangler d1 migrations apply PROJECT_NAME-db --remote" } ``` ## Workflow ### Step 1: Gather Project Info | Required | Optional | |----------|----------| | Project name (kebab-case) | Google OAuth credentials | | One-line description | Custom domain | | Cloudflare account | R2 storage needed? | | Auth method: Google OAuth, email/password, or both | Admin email | ### Step 2: Initialise Project Create the project directory and all config files from scratch. **`vite.config.ts`** — Plugin order matters. Cloudflare MUST be first: ```typescript import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import tailwindcss from "@tailwindcss/vite"; import viteReact from "@vitejs/plugin-react"; export default defineConfig({ plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), tailwindcss(), tanstackStart(), viteReact(), ], }); ``` **`wrangler.jsonc`**: ```jsonc { "$schema": "node_modules/wrangler/config-schema.json", "name": "PROJECT_NAME", "compatibility_date": "2025-04-01", "compatibility_flags": ["nodejs_compat"], "main": "@tanstack/react-start/server-entry", "account_id": "ACCOUNT_ID", "d1_databases": [ { "binding": "DB", "database_name": "PROJECT_NAME-db", "database_id": "DATABASE_ID", "migrations_dir": "drizzle" } ] } ``` Key points: `main` MUST be `"@tanstack/react-start/server-entry"` (Nitro server entry). Use `nodejs_compat` (NOT `node_compat`). Add `account_id` to avoid interactive prompts. **`tsconfig.json`**: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "paths": { "@/*": ["./src/*"] }, "types": ["@cloudflare/workers-types/2023-07-01"] }, "include": ["src/**/*", "vite.config.ts"] } ``` **`.dev.vars`** — generate `BETTER_AUTH_SECRET` with `openssl rand -hex 32`: ``` BETTER_AUTH_SECRET=<generated-hex-32> BETTER_AUTH_URL=http://localhost:3000 TRUSTED_ORIGINS=http://localhost:3000 # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= ``` **`.gitignore`** — node_modules, .wrangler, dist, .output, .dev.vars, .vinxi, .DS_Store Then install and create the D1 database: ```bash cd PROJECT_NAME && pnpm install npx wrangler d1 create PROJECT_NAME-db # Copy the database_id into wrangler.jsonc d1_databases binding ``` ### Step 3: Database Schema **`src/db/schema.ts`** — All tables. better-auth requires: `users`, `sessions`, `accounts`, `verifications`. Add application tables (e.g. `items`) for CRUD demo. D1-specific rules: - Use `integer` for timestamps (Unix epoch), NOT Date objects - Use `text` for primary keys (nanoid/cuid2), NOT autoincrement - Keep bound parameters under 100 per query (batch large inserts) - Foreign keys are always ON in D1 **`src/db/index.ts`** — Drizzle client factory: ```typescript import { drizzle } from "drizzle-orm/d1"; import { env } from "cloudflare:workers"; import * as schema from "./schema"; export function getDb() { return drizzle(env.DB, { schema }); } ``` **CRITICAL**: Use `import { env } from "cloudflare:workers"` — NOT `process.env`. Create the Drizzle client inside each server function (per-request), not at module level. **`drizzle.config.ts`**: ```typescript import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/db/schema.ts", out: "./drizzle", dialect: "sqlite", }); ``` Generate and apply the initial migration: ```bash pnpm db:generate pnpm db:migrate:local ``` ### Step 4: Configure Auth **`src/lib/auth.server.ts`** — Server-side better-auth: ```typescript import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzle } from "drizzle-orm/d1"; import { env } from "cloudflare:workers"; import * as schema from "../db/schema"; export function getAuth() { const db = drizzle(env.DB, { schema }); return betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }), secret: env.BETTER_AUTH_SECRET, baseURL: env.BETTER_AUTH_URL, trustedOrigins: env.TRUSTED_ORIGINS?.split(",") ?? [], emailAndPassword: { enabled: true }, socialProviders: { // Add Google OAuth if credentials provided }, }); } ``` **CRITICAL**: `getAuth()` must be called per-request (inside handler/loader), NOT at module level. **`src/lib/auth.client.ts`** — Client-side auth hooks: ```typescript import { createAuthClient } from "better-auth/react"; export const { useSession, signIn, signOut, signUp } = createAuthClient(); ``` **`src/routes/api/auth/$.ts`** — API catch-all for better-auth: ```typescript import { createAPIFileRoute } from "@tanstack/react-start/api"; import { getAuth } from "../../../lib/auth.server"; export const APIRoute = createAPIFileRoute("/api/auth/$")({ GET: ({ request }) => getAuth().handler(request), POST: ({ request }) => getAuth().handler(request), }); ``` **CRITICAL**: Auth MUST use an API route (`createAPIFileRoute`), NOT a server function (`createServerFn`). better-auth needs direct request/response access. ### Step 5: Server Functions **Core pattern** — always create DB client inside the handler: ```typescript import { createServerFn } from "@tanstack/react-start"; import { getDb } from "../db"; export const getItems = createServerFn({ method: "GET" }).handler(async () => { const db = getDb(); return db.select().from(items).all(); }); ``` **Input validation** with Zod: ```typescript export const createItem = createServerFn({ method: "POST" }) .inputValidator( z.object({ name: z.string().min(1), description: z.string().optional(), }) ) .handler(async ({ data }) => { const db = getDb(); const id = crypto.randomUUID(); await db.insert(items).values({ id, ...data, createdAt: Date.now() }); return { id }; }); ``` **Protected server functions** — check auth, throw redirect if unauthenticated: ```typescript import { redirect } from "@tanstack/react-router"; import { getAuth } from "../lib/auth.server"; async function requireSession(request?: Request) { const auth = getAuth(); const session = await auth.api.getSession({ headers: request?.headers ?? new Headers(), }); if (!session) { throw redirect({ to: "/login" }); } return session; } export const getSessionFn = createServerFn({ method: "GET" }).handler( async ({ request }) => { const auth = getAuth(); return auth.api.getSession({ headers: request.headers }); } ); export const getItems = createServerFn({ method: "GET" }).handler(
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub