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. No template repo — Claude generates every file fresh per project.
Generates full-stack TanStack Start applications on Cloudflare Workers with SSR, auth, database, and UI from scratch.
npx claudepluginhub jezweb/claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/architecture.mdreferences/deployment.mdreferences/server-functions.mdBuild a complete full-stack app from nothing. Claude generates every file — no template clone, no scaffold command. Each project gets exactly what it needs.
| Layer | Technology |
|---|---|
| Framework | TanStack Start v1 (SSR, file-based routing, server functions) |
| Frontend | React 19, Tailwind v4, shadcn/ui |
| Backend | Server functions (via Nitro on Cloudflare Workers) |
| Database | D1 + Drizzle ORM |
| Auth | better-auth (Google OAuth + email/password) |
| Deployment | Cloudflare Workers |
Ask for:
| 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 |
Create the project directory and all config files from scratch.
See references/architecture.md for the complete file tree, all dependencies, and config templates.
Create these files first:
package.json — all runtime + dev dependencies with version ranges from architecture.mdtsconfig.json — strict mode, @/* path alias mapped to src/*vite.config.ts — plugins in correct order: cloudflare() → tailwindcss() → tanstackStart() → viteReact()wrangler.jsonc — main: "@tanstack/react-start/server-entry", nodejs_compat flag, D1 binding placeholder.dev.vars — generate BETTER_AUTH_SECRET with openssl rand -hex 32, set BETTER_AUTH_URL=http://localhost:3000, TRUSTED_ORIGINS=http://localhost:3000.gitignore — node_modules, .wrangler, dist, .output, .dev.vars, .vinxi, .DS_StoreThen:
cd PROJECT_NAME
pnpm install
Create D1 database and update wrangler.jsonc:
npx wrangler d1 create PROJECT_NAME-db
# Copy the database_id into wrangler.jsonc d1_databases binding
Create the Drizzle schema with D1-correct patterns.
src/db/schema.ts — Define all tables:
users, sessions, accounts, verifications — these are required by better-authitems (or whatever the project needs) for CRUD demoD1-specific rules:
integer for timestamps (Unix epoch), NOT Date objectstext for primary keys (nanoid/cuid2), NOT autoincrementsrc/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. This is a per-request binding, so create the Drizzle client inside each server function, not at module level.
drizzle.config.ts:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "sqlite",
});
Add migration scripts to package.json:
{
"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"
}
Generate and apply the initial migration:
pnpm db:generate
pnpm db:migrate:local
src/lib/auth.server.ts — Server-side better-auth configuration:
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. The env import from cloudflare:workers is only available during request handling.
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 route 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.
src/routes/__root.tsx — Root layout with HTML document:
<HeadContent /> and <Scripts /> from @tanstack/react-routersuppressHydrationWarning on <html> for SSR + theme toggle compatibilitysrc/styles/app.css — Tailwind v4 + shadcn/ui:
@import "tailwindcss" (v4 syntax):root and .darksrc/router.tsx — Router configuration:
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 and src/ssr.tsx — Entry points (standard TanStack Start boilerplate).
Install shadcn/ui components needed for the dashboard:
pnpm dlx shadcn@latest init --defaults
pnpm dlx shadcn@latest add button card input label sidebar table dropdown-menu form separator sheet
Note: Configure shadcn to use src/components as the components directory.
Theme toggle: three-state (light → dark → system → light). Store preference in localStorage. Apply .dark class on <html>. Use JS-only system preference detection — NO CSS @media (prefers-color-scheme) queries.
Create the route files:
src/routes/
├── __root.tsx # Root layout (HTML shell, theme, CSS)
├── index.tsx # Landing → redirect to /dashboard if authenticated
├── login.tsx # Login form (email/password + Google OAuth button)
├── register.tsx # Registration form
├── _authed.tsx # Auth guard layout (beforeLoad checks session)
└── _authed/
├── dashboard.tsx # Stat cards overview
├── items.tsx # Items list (table with actions)
├── items.$id.tsx # Edit item form
└── items.new.tsx # Create item form
Auth guard pattern (_authed.tsx):
import { createFileRoute, redirect } from "@tanstack/react-router";
import { getSessionFn } from "../server/auth";
export const Route = createFileRoute("/_authed")({
beforeLoad: async () => {
const session = await getSessionFn();
if (!session) {
throw redirect({ to: "/login" });
}
return { session };
},
});
Components (in src/components/):
app-sidebar.tsx — shadcn Sidebar with navigation links (Dashboard, Items)theme-toggle.tsx — three-state theme toggle buttonuser-nav.tsx — user dropdown menu with sign-out actionstat-card.tsx — reusable stat card for the dashboardSee references/server-functions.md for createServerFn patterns used in route loaders and mutations.
Create server functions for the items resource:
| 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:
getDb()Route loaders call GET server functions. Mutations call POST server functions then router.invalidate() to refetch.
pnpm dev
Verification checklist:
# 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
# If using Google OAuth
echo "your-client-id" | npx wrangler secret put GOOGLE_CLIENT_ID
echo "your-client-secret" | npx wrangler secret put GOOGLE_CLIENT_SECRET
# Migrate remote database
pnpm db:migrate:remote
# Build and deploy
pnpm build && npx wrangler deploy
After first deploy: Update BETTER_AUTH_URL with your actual Worker URL. Add production URL to Google OAuth redirect URIs: https://your-app.workers.dev/api/auth/callback/google.
See references/deployment.md for the full production checklist and common mistakes.
| Symptom | Fix |
|---|---|
env is undefined in server function | Use import { env } from "cloudflare:workers" — must be inside request handler, not module scope |
| D1 database not found | Check wrangler.jsonc d1_databases binding name matches code |
| Auth redirect loop | BETTER_AUTH_URL must match actual URL exactly (protocol + domain, no trailing slash) |
| Auth silently fails (redirects to home) | Set TRUSTED_ORIGINS secret with all valid URLs (comma-separated) |
| Styles not loading in dev | Ensure @tailwindcss/vite plugin is in vite.config.ts |
| SSR hydration mismatch | Add suppressHydrationWarning to <html> element |
| Build fails on Cloudflare | Check nodejs_compat in compatibility_flags, main field in wrangler.jsonc |
| Secrets not taking effect | wrangler secret put does NOT redeploy — run npx wrangler deploy after |
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.