一键导入
tanstack-start-cloudflare
Build full-stack apps with TanStack Start on Cloudflare Workers — routing, server functions, middleware, better-auth, Drizzle ORM with D1
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build full-stack apps with TanStack Start on Cloudflare Workers — routing, server functions, middleware, better-auth, Drizzle ORM with D1
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Write atomic, human-reviewable git commits — one logical change per commit with clear conventional messages
Implement canonical log lines / wide events pattern — one structured JSON line per request with full context, sampling, and observability integration
基于 SOC 职业分类
| name | tanstack-start-cloudflare |
| description | Build full-stack apps with TanStack Start on Cloudflare Workers — routing, server functions, middleware, better-auth, Drizzle ORM with D1 |
| version | 1.0.0 |
Build full-stack TypeScript apps with TanStack Start deployed to Cloudflare Workers. Covers routing, server functions with middleware, authentication via better-auth, and Drizzle ORM with D1.
src/
├── routes/ # File-based routes (TanStack Router)
│ ├── __root.tsx # Root layout — html, head, body
│ ├── index.tsx # Landing page
│ ├── _protected.tsx # Auth guard layout (pathless)
│ ├── _protected/ # All authenticated routes
│ │ └── dashboard/
│ └── api/ # API route handlers
│ └── v1/
├── lib/
│ ├── auth/ # better-auth config (server, client, middleware)
│ ├── db/ # Drizzle schema + queries
│ ├── middleware/ # Composable middleware (logging, cloudflare, auth)
│ └── api/ # API helpers (response builders, logging)
├── components/ # React components
├── styles/ # Tailwind CSS
└── router.tsx # Router config — exports getRouter()
Server functions use createServerFn with a composable middleware chain:
import { createServerFn } from "@tanstack/react-start";
import {
loggingMiddleware,
cloudflareMiddleware,
authMiddleware,
} from "~/lib/middleware";
// Authenticated operation
export const getMySkills = createServerFn({ method: "GET" })
.middleware([loggingMiddleware, cloudflareMiddleware, authMiddleware])
.handler(async ({ context }) => {
// context.cloudflare.env — typed bindings (DB, R2, KV)
// context.session.user — authenticated user
// context.logger — request-scoped logger
const db = drizzle(context.cloudflare.env.DB);
return db.select().from(skills)
.where(eq(skills.ownerId, context.session.user.id));
});
env bindings (DB, SKILLS_BUCKET, CACHE)import type { LoggedAuthContext } from "~/lib/middleware/types";
// Handler receives the accumulated context from all middleware
.handler(async ({ context }: { context: LoggedAuthContext }) => {
context.cloudflare.env.DB; // D1Database
context.cloudflare.env.SKILLS_BUCKET; // R2Bucket
context.session.user.id; // string
context.logger; // Logger instance
});
export const updateProfile = createServerFn({ method: "POST" })
.middleware([loggingMiddleware, cloudflareMiddleware, authMiddleware])
.validator((data: { displayName: string }) => data)
.handler(async ({ context, data }) => {
// data is typed as { displayName: string }
});
[[d1_databases]]
binding = "DB"
database_name = "my-db"
[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-bucket"
[[kv_namespaces]]
binding = "CACHE"
id = "abc123"
Middleware provides typed access — never import env directly in handlers:
// Correct — use middleware context
const db = drizzle(context.cloudflare.env.DB);
// Wrong — don't do this in handlers
import { env } from "cloudflare:workers"; // only for middleware internals
Define once in src/lib/middleware/types.ts:
export interface CloudflareEnv {
DB: D1Database;
SKILLS_BUCKET: R2Bucket;
CACHE: KVNamespace;
APP_URL: string;
}
// src/lib/auth/server.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
export const auth = betterAuth({
database: drizzleAdapter(drizzle(env.DB), { provider: "sqlite" }),
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
},
});
// src/routes/api/auth/$.ts
import { createAPIFileRoute } from "@tanstack/react-start/api";
import { auth } from "~/lib/auth/server";
export const APIRoute = createAPIFileRoute("/api/auth/$")({
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
});
Use a pathless layout route as an auth guard:
// src/routes/_protected.tsx
export const Route = createFileRoute("/_protected")({
beforeLoad: async () => {
const session = await checkAuthFn();
if (!session) throw redirect({ to: "/login" });
return { session };
},
component: () => <Outlet />, // Just passes through
});
All routes under _protected/ inherit the guard automatically.
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
db.batch() not db.transaction() — D1 doesn't support SQL BEGIN/COMMITinteger with mode: 'timestamp' for dates (no native DATE type in SQLite)onDelete: 'cascade' for cleanupimport { drizzle } from "drizzle-orm/d1";
import { eq } from "drizzle-orm";
const db = drizzle(context.cloudflare.env.DB);
const result = await db.select().from(users).where(eq(users.id, userId));
import { cloudflare } from "@cloudflare/vite-plugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
tanstackStart(),
],
});
nvm use.routeTree.gen.ts auto-generates on first pnpm dev.?url suffix — import appCss from "../styles/app.css?url".@plugin "@tailwindcss/typography" not @import.main = "@tanstack/react-start/server-entry".strict-peer-dependencies=false in .npmrc for TanStack ecosystem.