
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.
- Installs
- 2.5K
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/jezweb/claude-skills --skill tanstack-start- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads tanstack-start automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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:
{
"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:
{
"@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:
{
"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:
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:
{
"$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:
{
"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:
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
integerfor timestamps (Unix epoch), NOT Date objects - Use
textfor 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:
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:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "sqlite",
});
Generate and apply the initial migration:
pnpm db:generate
pnpm db:migrate:local
Step 4: Configure Auth
src/lib/auth.server.ts — Server-side better-auth:
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:
import { createAuthClient } from "better-auth/react";
export const { useSession, signIn, signOut, signUp } = createAuthClient();
src/routes/api/auth/$.ts — API catch-all for better-auth:
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:
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:
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:
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(
async ({ request }) => {
const session = await requireSession(request);
const db = getDb();
return db.select().from(items).where(eq(items.userId, session.user.id)).all();
}
);
Route loader pattern — server functions in route loader:
export const Route = createFileRoute("/_authed/items")({
loader: () => getItems(),
component: ItemsPage,
});
function ItemsPage() {
const items = Route.useLoaderData();
return <div>{items.map((item) => <div key={item.id}>{item.name}</div>)}</div>;
}
Auth guard (_authed.tsx) — use beforeLoad:
export const Route = createFileRoute("/_authed")({
beforeLoad: async () => {
const session = await getSessionFn();
if (!session) {
throw redirect({ to: "/login" });
}
return { session };
},
});
Child routes access session via Route.useRouteContext().
Mutation + invalidation — after mutations, invalidate router to refetch loaders:
function CreateItemForm() {
const router = useRouter();
const handleSubmit = async (data: NewItem) => {
await createItem({ data });
router.invalidate();
router.navigate({ to: "/items" });
};
return <form onSubmit={...}>...</form>;
}
Type safety — use Drizzle's InferSelectModel / InferInsertModel for server function input/output types. For auth failures, always use throw redirect() — not error responses.
Step 6: App Shell + Theme
src/routes/__root.tsx — Full HTML document with <HeadContent /> + <Scripts /> from @tanstack/react-router, suppressHydrationWarning on <html> (SSR + theme), inline theme init script to prevent flash, global CSS import.
src/styles/app.css — @import "tailwindcss" (v4 syntax) + shadcn/ui CSS variables in :root and .dark. Semantic tokens only.
src/router.tsx:
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function createRouter() {
return createTanStackRouter({ routeTree });
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
src/client.tsx + src/ssr.tsx — standard TanStack Start entry boilerplate.
Install shadcn/ui:
pnpm dlx shadcn@latest init --defaults
pnpm dlx shadcn@latest add button card input label sidebar table dropdown-menu form separator sheet
Theme toggle — three-state (light → dark → system → light), localStorage-persisted, .dark class on <html>. JS-only system preference detection; NO CSS @media (prefers-color-scheme) queries.
Components in src/components/: app-sidebar.tsx, theme-toggle.tsx, user-nav.tsx, stat-card.tsx.
Step 7: CRUD Server Functions
| Function | Method | Purpose |
|---|---|---|
getItems | GET | List all items for current user |
getItem | GET | Get single item by ID |
createItem | POST | Create new item |
updateItem | POST | Update existing item |
deleteItem | POST | Delete item by ID |
Each server function: (1) gets auth session, (2) creates per-request Drizzle client via getDb(), (3) performs DB operation, (4) returns typed data. Route loaders call GET functions. Mutations call POST functions then router.invalidate().
Step 8: Verify Locally
pnpm dev
- App loads at http://localhost:3000
- Register a new account (email/password)
- Login and logout work
- Dashboard loads with stat cards
- Create, list, edit, delete items
- Theme toggle cycles: light -> dark -> system
- Sidebar collapses on mobile
- No console errors
Step 9: Deploy to Production
Pre-deploy checklist — verify before running deploy:
-
wrangler.jsonchas correctaccount_id;mainis"@tanstack/react-start/server-entry";nodejs_compatincompatibility_flags - D1 database created and
database_idset -
.dev.varsis gitignored; no hardcoded secrets in source
Set production secrets:
openssl rand -hex 32 | npx wrangler secret put BETTER_AUTH_SECRET
echo "https://PROJECT.SUBDOMAIN.workers.dev" | npx wrangler secret put BETTER_AUTH_URL
echo "http://localhost:3000,https://PROJECT.SUBDOMAIN.workers.dev" | npx wrangler secret put TRUSTED_ORIGINS
# Google OAuth (optional)
echo "your-client-id" | npx wrangler secret put GOOGLE_CLIENT_ID
echo "your-client-secret" | npx wrangler secret put GOOGLE_CLIENT_SECRET
If using Google OAuth, add the production redirect URI in Google Cloud Console: https://PROJECT.SUBDOMAIN.workers.dev/api/auth/callback/google.
Migrate and deploy:
pnpm db:migrate:remote
pnpm build && npx wrangler deploy
After first deploy, update BETTER_AUTH_URL to the actual Worker URL and redeploy.
Verify: app loads at production URL, auth works, CRUD works, theme persists.
Custom domain (optional): Cloudflare Dashboard → Workers → Triggers → Custom Domains. Update BETTER_AUTH_URL + TRUSTED_ORIGINS secrets + Google OAuth redirect URI to the new domain. Redeploy.
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
env is undefined | Accessed at module level | Use import { env } from "cloudflare:workers" inside request handler only |
| D1 database not found | Binding mismatch | Check d1_databases binding name in wrangler.jsonc matches code |
| Auth redirect loop | URL mismatch | BETTER_AUTH_URL must match actual URL exactly (protocol + domain, no trailing slash) |
| Auth silently fails | Missing origins | Set TRUSTED_ORIGINS secret with all valid URLs (comma-separated) |
| Styles not loading | Missing plugin | Ensure @tailwindcss/vite plugin is in vite.config.ts |
| SSR hydration mismatch | Theme flash | Add suppressHydrationWarning to <html> element |
| Build fails on Cloudflare | Bad config | Check nodejs_compat flag and main field in wrangler.jsonc |
| Secrets not taking effect | No redeploy | wrangler secret put does NOT redeploy — run npx wrangler deploy after |
| Auth endpoints return 404 | Wrong route type | Use createAPIFileRoute (API route), not createServerFn for better-auth |
| "redirect_uri_mismatch" | Missing URI | Add production URL to Google Cloud Console OAuth redirect URIs |
| Cryptic Vite errors | Plugin order | Must be: cloudflare() -> tailwindcss() -> tanstackStart() -> viteReact() |
| "Table not found" 500s | Missing migration | Run pnpm db:migrate:remote before deploying |
FAQ
What does the tanstack-start skill do?
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.
How do I install tanstack-start?
Run npx skills add https://github.com/jezweb/claude-skills --skill tanstack-start in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/tanstack-start/ (personal) or .claude/skills/tanstack-start/ (project).
Which AI agents can use tanstack-start?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is tanstack-start safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains tanstack-start?
It is published by jezweb in jezweb/claude-skills. It has 2,493 recorded installs.
More from jezweb/claude-skills
shadcn-ui
jezweb/claude-skills
Install and configure shadcn/ui components for React projects. Guides component selection, installation order, dependency management, customisation with semantic tokens, and common UI recipes (forms, data tables, navigation, modals). Use after tailwind-theme-builder has set up the theme infrastructure, when adding components, building forms, creating data tables, or setting up navigation.
color-palette
jezweb/claude-skills
Generate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a design system, wants Tailwind theme colours from a brand colour, or asks to check colour accessibility / contrast.
tailwind-theme-builder
jezweb/claude-skills
Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues.
wordpress-elementor
jezweb/claude-skills
Edit Elementor pages and manage templates on WordPress sites via browser automation (for visual / structural changes) or WP-CLI (for safe text replacements). Use whenever the user wants to edit an Elementor page, update text in Elementor widgets, apply or manage Elementor templates, or make content changes to pages built with the Elementor page builder.
ux-audit
jezweb/claude-skills
Walk through a live web app AS a real user to find usability + behavioural bugs that static reviews miss. REQUIRES proof of interaction (typing, clicking, sending, observing) before any verdict — a sweep that didn't interact terminates with verdict 'Incomplete'. Walks threads, exercises every element, runs the multi-pane stress matrix, visual polish sweep, component perfection checklist, automated a11y (axe-core), pragmatic performance budget (LCP/CLS/INP), scenario battery (11 scenarios), and stress recipes including the real-flavour data battery. Hard gates: console errors/warnings = 0, network 5xx = 0, layout collapse = 0, axe Critical/Serious = 0, perf budget green. Audit-the-audit meta-check rejects rushed reports. Each finding has reproduction steps, evidence path, and suspected code location. Trigger with 'ux audit', 'walkthrough', 'qa sweep', 'audit the app', 'dogfood this', 'check all pages', 'find what's broken', 'stress the UI'.
tailwind-v4-shadcn
jezweb/claude-skills
Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step pattern: CSS variables, Tailwind mapping, base styles, automatic dark mode. Prevents 8 documented errors. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, @theme inline dark mode conflicts, @apply breaking, v3 migration issues.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.