Get the FREE Ultimate OpenClaw Setup Guide →

tanstack-start

Scanned
npx machina-cli add skill jezweb/claude-skills/tanstack-start --openclaw
Files (1)
SKILL.md
11.4 KB

TanStack Start on Cloudflare

Build a complete full-stack app from nothing. Claude generates every file — no template clone, no scaffold command. Each project gets exactly what it needs.

What You Get

LayerTechnology
FrameworkTanStack Start v1 (SSR, file-based routing, server functions)
FrontendReact 19, Tailwind v4, shadcn/ui
BackendServer functions (via Nitro on Cloudflare Workers)
DatabaseD1 + Drizzle ORM
Authbetter-auth (Google OAuth + email/password)
DeploymentCloudflare Workers

Workflow

Step 1: Gather Project Info

Ask for:

RequiredOptional
Project name (kebab-case)Google OAuth credentials
One-line descriptionCustom domain
Cloudflare accountR2 storage needed?
Auth method: Google OAuth, email/password, or bothAdmin email

Step 2: Initialise Project

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:

  1. package.json — all runtime + dev dependencies with version ranges from architecture.md
  2. tsconfig.json — strict mode, @/* path alias mapped to src/*
  3. vite.config.ts — plugins in correct order: cloudflare()tailwindcss()tanstackStart()viteReact()
  4. wrangler.jsoncmain: "@tanstack/react-start/server-entry", nodejs_compat flag, D1 binding placeholder
  5. .dev.vars — generate BETTER_AUTH_SECRET with openssl rand -hex 32, set BETTER_AUTH_URL=http://localhost:3000, TRUSTED_ORIGINS=http://localhost:3000
  6. .gitignore — node_modules, .wrangler, dist, .output, .dev.vars, .vinxi, .DS_Store

Then:

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

Step 3: Database Schema

Create the Drizzle schema with D1-correct patterns.

src/db/schema.ts — Define all tables:

  • better-auth tables: users, sessions, accounts, verifications — these are required by better-auth
  • Application table: items (or whatever the project needs) 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:

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

Step 4: Configure Auth

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.

Step 5: App Shell + Theme

src/routes/__root.tsx — Root layout with HTML document:

  • Render full HTML document with <HeadContent /> and <Scripts /> from @tanstack/react-router
  • Add suppressHydrationWarning on <html> for SSR + theme toggle compatibility
  • Import the global CSS file
  • Include theme initialisation script inline to prevent flash of wrong theme

src/styles/app.css — Tailwind v4 + shadcn/ui:

  • @import "tailwindcss" (v4 syntax)
  • CSS variables for shadcn/ui tokens in :root and .dark
  • Neutral/monochrome palette (stone, slate, zinc)
  • Use semantic tokens only — never raw Tailwind colours

src/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.

Step 6: Routes + Dashboard

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 button
  • user-nav.tsx — user dropdown menu with sign-out action
  • stat-card.tsx — reusable stat card for the dashboard

See references/server-functions.md for createServerFn patterns used in route loaders and mutations.

Step 7: CRUD Server Functions

Create server functions for the items resource:

FunctionMethodPurpose
getItemsGETList all items for current user
getItemGETGet single item by ID
createItemPOSTCreate new item
updateItemPOSTUpdate existing item
deleteItemPOSTDelete item by ID

Each server function:

  1. Gets auth session (redirect if not authenticated)
  2. Creates per-request Drizzle client via getDb()
  3. Performs the database operation
  4. Returns typed data

Route loaders call GET server functions. Mutations call POST server functions then router.invalidate() to refetch.

Step 8: Verify Locally

pnpm dev

Verification checklist:

  • App loads at http://localhost:3000
  • Register a new account (email/password)
  • Login and logout work
  • Dashboard page loads with stat cards
  • Create a new item via /items/new
  • Items list shows the new item
  • Edit item via /items/:id
  • Delete item from the list
  • Theme toggle cycles: light → dark → system
  • Sidebar collapses on mobile viewports
  • No console errors

Step 9: Deploy to Production

# 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.

Common Issues

SymptomFix
env is undefined in server functionUse import { env } from "cloudflare:workers" — must be inside request handler, not module scope
D1 database not foundCheck wrangler.jsonc d1_databases binding name matches code
Auth redirect loopBETTER_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 devEnsure @tailwindcss/vite plugin is in vite.config.ts
SSR hydration mismatchAdd suppressHydrationWarning to <html> element
Build fails on CloudflareCheck nodejs_compat in compatibility_flags, main field in wrangler.jsonc
Secrets not taking effectwrangler secret put does NOT redeploy — run npx wrangler deploy after

Source

git clone https://github.com/jezweb/claude-skills/blob/main/plugins/cloudflare/skills/tanstack-start/SKILL.mdView on GitHub

Overview

Build a complete TanStack Start app on Cloudflare Workers from scratch. Claude generates every file—no template repos—delivering SSR, file-based routing, and server functions wired to D1 + Drizzle, with React 19, Tailwind v4 + shadcn/ui and better-auth for Google OAuth or email/password.

How This Skill Works

Claude collects project details, then creates the project from scratch with all essential config files. It sets up D1 + Drizzle, wires server functions with per-request env bindings, and enables better-auth plus Tailwind v4 + shadcn/ui for a Cloudflare Workers deployment with SSR.

When to Use It

  • Starting a new TanStack Start project on Cloudflare Workers with no template repo.
  • You need SSR, file-based routing, and server functions backed by D1 + Drizzle.
  • You require Google OAuth or email/password authentication via better-auth.
  • You want React 19 with Tailwind v4 and shadcn/ui integrated in a full-stack workflow.
  • You plan to deploy directly to Cloudflare Workers and manage migrations with drizzle-kit.

Quick Start

  1. Step 1: Gather project info (name in kebab-case, one-line description, Cloudflare account, auth method, etc.).
  2. Step 2: Initialise the project by creating all required files (package.json, tsconfig.json, vite.config.ts, wrangler.jsonc, .dev.vars, .gitignore) and installing dependencies; set up D1 and update wrangler.jsonc.
  3. Step 3: Implement DB schema and auth wiring (Drizzle schema.ts, Drizzle client, drizzle.config.ts, and migrate scripts; configure better-auth).

Best Practices

  • Follow the Step 2 file creation order (package.json, tsconfig.json, vite.config.ts, wrangler.jsonc, .dev.vars, .gitignore).
  • Create the Drizzle client per request inside server functions using env (not at module scope).
  • Define D1 schema to use integer timestamps, text primary keys, and ON foreign keys; keep bound params under 100.
  • Use the provided npm scripts (db:generate, db:migrate:local, db:migrate:remote) to manage migrations.
  • Consult references/architecture.md for architecture-specific files and dependencies to align templates with your project.

Example Use Cases

  • Admin dashboard with items CRUD and better-auth user management.
  • Team project dashboard with Google OAuth and per-user data.
  • Blog platform with SSR, posts, and comments stored in D1.
  • Inventory management app for a small retailer using D1 + Drizzle.
  • Product catalog with server-driven queries and real-time updates.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers