Install
$ agentstack add skill-khaledgarbaya-skillvault-tanstack-start-cloudflare ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
TanStack Start on Cloudflare Workers
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.
Stack Overview
- Framework: TanStack Start (stable v1.x) — file-based routing, SSR, server functions
- Runtime: Cloudflare Workers — edge compute, no cold starts
- Database: Cloudflare D1 (SQLite) via Drizzle ORM
- Auth: better-auth with session + token strategies
- Bundler: Vite 7 with Cloudflare plugin
- UI: React 19 + Tailwind CSS v4 + shadcn/ui
Project Structure
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 with Middleware
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));
});
Middleware Chain Order
- loggingMiddleware — creates Logger, starts timer, flushes wide event on completion
- cloudflareMiddleware — exposes
envbindings (DB, SKILLS_BUCKET, CACHE) - authMiddleware — validates session, enriches logger with user identity
Typed Context
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
});
Input Validation
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 }
});
Cloudflare Environment
Bindings (wrangler.toml)
[[d1_databases]]
binding = "DB"
database_name = "my-db"
[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-bucket"
[[kv_namespaces]]
binding = "CACHE"
id = "abc123"
Access in Server Functions
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
Type Definition
Define once in src/lib/middleware/types.ts:
export interface CloudflareEnv {
DB: D1Database;
SKILLS_BUCKET: R2Bucket;
CACHE: KVNamespace;
APP_URL: string;
}
Authentication with better-auth
Server Config
// 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,
},
},
});
Catch-All Route
// 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),
});
Protected Routes
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: () => , // Just passes through
});
All routes under _protected/ inherit the guard automatically.
Drizzle ORM with D1
Schema Definition
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()),
});
D1 Constraints
- Use
db.batch()notdb.transaction()— D1 doesn't support SQL BEGIN/COMMIT - Use
integerwithmode: 'timestamp'for dates (no native DATE type in SQLite) - Foreign keys use
onDelete: 'cascade'for cleanup
Queries
import { 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));
Vite Configuration
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(),
],
});
Key Gotchas
- Node version: Vite 7 requires Node 20.19+ or 22.12+. Always
nvm use. - Route tree:
routeTree.gen.tsauto-generates on firstpnpm dev. - CSS imports: Use
?urlsuffix —import appCss from "../styles/app.css?url". - Tailwind v4 plugins: Use
@plugin "@tailwindcss/typography"not@import. - Wrangler entry: Set
main = "@tanstack/react-start/server-entry". - Peer deps: Set
strict-peer-dependencies=falsein.npmrcfor TanStack ecosystem.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Khaledgarbaya
- Source: Khaledgarbaya/skillvault
- License: MIT
- Homepage: https://skvault.dev
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.