Essential instructions for AI coding agents working on this project. For initial setup, see
docs/AGENTS_SETUP.md.
The project is based on a SaaS starter template built on Cloudflare's edge platform with:
- Frontend: React 19 + TypeScript + Vite + Tailwind CSS + shadcn/ui
- Backend: Hono on Cloudflare Workers
- Database: Cloudflare D1 (SQLite at the edge)
- Auth: Better Auth (email/password with verification)
- Email: Cloudflare Email Service or Resend (pluggable provider)
The @cloudflare/vite-plugin unifies frontend and backend in one dev server:
- Single server at http://localhost:5173
- API routes at
/api/*handled by Hono - Hot reload works for both frontend and backend
- Never try to run separate servers or configure proxies
Never import Kysely or access D1 directly in frontend code. Always use HTTP APIs:
// ✅ Correct - use API
const response = await fetch('/api/users');
// ❌ Wrong - direct database access
import { Kysely } from 'kysely';src/
├── react-app/ # React frontend
│ ├── assets/ # Static assets (logos, images)
│ │ ├── logo-light.svg # Logo for light theme
│ │ └── logo-dark.svg # Logo for dark theme
│ ├── components/
│ │ ├── auth/ # Auth UI (LoginForm, ProtectedRoute, AuthOverlay)
│ │ ├── ui/ # shadcn/ui components
│ │ ├── TopBar.tsx # Full-width header with logo and user menu
│ │ ├── Logo.tsx # Theme-aware logo component
│ │ ├── ModeToggle.tsx # Dark/light theme toggle
│ │ └── theme-provider.tsx # Theme context with URL param support
│ ├── pages/ # Route pages
│ │ ├── auth/ # SignUp, VerifyEmail, ForgotPassword, ResetPassword
│ │ ├── Home.tsx # Protected home
│ │ └── Profile.tsx # User settings
│ └── lib/
│ └── auth/client.ts # Better Auth React client
└── worker/ # Hono backend
├── index.ts # API routes + CLI endpoints
├── middleware/auth.ts # Better Auth config
├── types/database.ts # Kysely types
└── utils/ # Email templates and provider adapters
src/cli/ # CLI commands (auth user management + db utilities)
src/config.ts # Global app configuration (app name, etc.)
bin/ # CLI entry points (init, auth, db)
migrations/ # D1 SQL migrations
seeds/ # Idempotent SQL seed files
Wrap routes with <ProtectedRoute> to require authentication:
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />Add routes in src/worker/index.ts:
// Public endpoint
app.get("/api/health", (c) => c.json({ status: "ok" }));
// Protected endpoint (requires auth)
app.use("/api/protected/*", authMiddleware);
app.get("/api/protected/data", async (c) => {
const user = c.get("user");
// ...
});Components are in @/components/ui/. Import directly:
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";See docs/SHADCN_COMPONENTS.md for the full component list.
Use Kysely in worker code:
import { Kysely } from "kysely";
import { D1Dialect } from "kysely-d1";
import type { Database } from "./types/database";
const db = new Kysely<Database>({
dialect: new D1Dialect({ database: c.env.DB }),
});
const users = await db.selectFrom("users").selectAll().execute();For remote database/auth commands,
.env.<env>is loaded automatically — no manualsourceneeded.
# Development
npm run dev # Start dev server (logs appear in terminal)
# Database — migrations
npm run db:migrate # Run migrations locally
npm run db:migrate:preview:safe # Migrate preview with pre-migration backup
npm run db:migrate:production:safe # Migrate production with pre-migration backup
# Database — migration status
npm run db:migrations:list
npm run db:migrations:list:preview
npm run db:migrations:list:production
# Database — backup & restore
npm run db:backup # Export local D1 to .wrangler/backups/
npm run db:backup:production # Export production D1 to .wrangler/backups/
npm run db:restore:latest # Restore latest backup (auto-backs-up current DB first)
npm run db:restore -- --env production --file prod-backup.sql
# Database — time-travel (remote envs only)
npm run db:time-travel:info -- --env production
npm run db:time-travel:restore -- --env production --timestamp "2026-04-01 12:00:00"
# Database — seeds
npm run db:seed -- --env local --file admin-bootstrap.sql
# Auth CLI
# Requires CLI_API_KEY in .env.<env>; key must also be deployed to the worker
# dev server must be running for local
# Available user management commands (run `npm run auth help <command>` to see options):
npm run auth list-users
npm run auth create-user
npm run auth edit-user
npm run auth delete-user
npm run auth activate-user
npm run auth set-role
npm run auth show-user
# Build & Deploy
npm run build # Build for production
npm run deploy:production # Deploy to Cloudflare (auto-syncs secrets)
# Logs (deployed environments only)
npm run tail:preview # Stream logs from preview
npm run tail:production # Stream logs from production- Create
src/react-app/pages/MyPage.tsx - Add route in
src/react-app/App.tsxwrapped with<ProtectedRoute>and<TopBar>if needed - Add navigation as appropriate for your app (TopBar doesn't include nav by default)
- Add route in
src/worker/index.ts - Use
authMiddlewareif authentication required - Access D1 via
c.env.DB
- Create migration in
migrations/ - Add types to
src/worker/types/database.ts - Run
npm run db:migrate - For remote envs, prefer
npm run db:migrate:preview:safe/npm run db:migrate:production:safe(takes a backup first)
Global settings shared by frontend and backend:
import { config } from "../config";
// or from worker: import { config } from "../../config";
console.log(config.appName); // "My App"
console.log(config.email.fromAddress); // "noreply@example.com"Update src/config.ts to customize app name, email settings, etc.
Configuration hierarchy:
src/config.ts— Global app config (app name, branding) - shared by frontend & backendwrangler.toml— Non-secret environment config (D1 bindings, ENVIRONMENT var).env.local/.env.preview/.env.production— Secrets per environment (gitignored)
How env files work:
.env.local— Loaded during local development (npm run dev).env.preview/.env.production— Synced to Cloudflare before deploy
Variable access by context:
// Client-side (React) - only VITE_ prefixed variables
const apiUrl = import.meta.env.VITE_API_URL;
// Server-side (Worker) - all variables from wrangler.toml + .env files
const emailProvider = c.env.EMAIL_PROVIDER;
const environment = c.env.ENVIRONMENT; // from wrangler.tomlDeploying secrets:
npm run deploy:preview/npm run deploy:productionautomatically syncs secrets from.env.<env>files- Only changed secrets are synced (tracked via hash file)
CLI auth (CLI_API_KEY):
- The auth CLI authenticates to
/api/cli/*using a sharedCLI_API_KEYsecret sent asx-api-key. - Generate one per environment:
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" - Set it in each
.env.<env>file; the deploy scripts sync it to the Worker automatically.
- Don't access database from frontend
- Don't run separate frontend/backend servers
- Don't modify shadcn/ui component internals (extend with wrappers)
- Don't hardcode URLs (use environment-based config)
Developers: Add project-specific instructions below as the app evolves. Keep this section updated with domain models, business logic, and conventions.
This file is read by AI coding agents. Keep it concise and actionable.