tanstack start

TanStack Start on Cloudflare

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack start" with this command: npx skills add jezweb/claude-skills/jezweb-claude-skills-tanstack-start

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

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

Workflow

Step 1: Gather Project Info

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

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:

  • package.json — all runtime + dev dependencies with version ranges from architecture.md

  • tsconfig.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_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:

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:

  • Gets auth session (redirect if not authenticated)

  • Creates per-request Drizzle client via getDb()

  • Performs the database operation

  • 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

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwind-v4-shadcn

No summary provided by upstream source.

Repository SourceNeeds Review
-2.7K
jezweb
General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
-2.5K
jezweb
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review