The harness-first, AI-first SaaS starter. Cloudflare Workers + React Router v7 + tRPC + D1 + Drizzle + Better Auth + Effect TS + ShadCN. Same runtime on your laptop and at the edge. Drop in any agent (Claude Code, Cursor, Codex) and ship real features by following retrieval-based docs, paste-able recipes, and deterministic verification gates.
Every task runs the same loop. Deterministic gates keep the agent coherent across sessions.
Three pillars:
- Harness-first β agents stay coherent across sessions via the 5-subsystem framework: instructions, state, verification, scope, lifecycle.
- AI-first β five project-local sub-agents, eight paste-able recipes, four deterministic slash commands, five grep-checkable non-negotiables.
- DX that just works β
bun install+bun run devand you're live.bun run scripts/first-time-setup.tsgets you from zero to deployed Worker in ~3 minutes. Local dev runs the same Workers runtime that ships to production.
π¦ Latest release: v1.2.0 β The Boilerplate Audited Itself
If you're a human, scroll to Quick Start. If you're an agent, scroll to How To Work In This Repo β it tells you which docs to open before writing code.
Stack
- Runtime: Cloudflare Workers (no Node), React Router v7 SSR
- Server logic: tRPC v11 procedures wrapped in Effect TS
- Persistence: D1 (SQLite) via Drizzle ORM 0.45, R2 for files
- Auth: Better Auth 1.4 with Drizzle adapter + admin plugin (RBAC)
- Validation: Effect Schema everywhere β no Zod
- Errors:
Data.TaggedErrormapped to tRPC codes viatagToTRPC - UI: ShadCN/Radix + Tailwind v4 (oklch), next-themes, react-hook-form + Effect resolver
- i18n: remix-i18next + i18next, route-level namespaces, fully typed
- Testing: Vitest 3 (unit) + Playwright 1.58 (e2e) +
@effect/vitest - Background: Cloudflare Workflows (
ExampleWorkflow)
Agent harness
.brain/β retrieval-first docs (rules, architecture, features, recipes, runs).brain/recipes/β paste-able runbooks bookended by00-before-task.md(init) and99-verify-done.md(termination check).brain/runs/β per-task continuity log so multi-session work survives compaction.githooks/pre-commitβ typecheck + tests block broken commits (no-dep, auto-installs).claude/hooks/brain-reminder.shβ staged-path β relevant-doc reminder before commit.claude/commands/verify-done.mdβ/verify-doneslash command runs the full termination checklistCLAUDE.md/AGENTS.mdβ kept byte-identical, both are the agent entry point
Real apps built on this starter β same runtime, same harness, same conventions:
| Project | What it is |
|---|---|
| home-karaoke π€ | Open-source group karaoke β host a room on the big screen, guests scan a QR to search YouTube and add songs to a live shared queue. Extends the base with Cloudflare Durable Objects for the real-time queue. |
| portfolio-v3 | Sean's personal portfolio β the starter stripped down to a content-first site on the edge. |
Shipped something on it? Open a PR adding it here.
# Bun (package manager + runtime)
curl -fsSL https://bun.sh/install | bash
# Cloudflare CLI
bun add -g wrangler
wrangler loginbun run scripts/first-time-setup.tsThe wizard creates your D1 database, R2 bucket, optional KV namespace, generates a BETTER_AUTH_SECRET, writes wrangler.jsonc + .env, runs migrations, and deploys. ~3 min end-to-end.
bun install # also runs cf-typegen + git hooks install
bun run db:migrate:local # apply migrations to local D1
bun run db:seed # seed local D1 with admin/user/banned fixtures
bun run dev # http://localhost:5173Read CLAUDE.md once. It points to everything else.
You're working in a codebase with strict conventions. Skipping the brain will cost rework. Every non-trivial task has three phases β each gated by a slash command.
1. Init β /start-task (or .brain/recipes/00-before-task.md)
- Runs
./init.sh --baselineto capture pre-change typecheck + test state - Reads the brain (matching
.brain/<folder>/index.md+ triggered files) - Frames task: intent, domain, scope, affected feature(s)
- Reads brain state via the
brainCLI (brain,brain progress,brain docs,brain search) - Validates scope policy via
brain features --status in-progress(brain checkenforces one-in-progress) β refuses if >1 feature in progress - Opens run note (
.brain/runs/<date>-<slug>.md) for >30min work - Appends entry to
.brain/runs/progress.md
2. Work β pick the runbook
- Adding code β matching recipe in
.brain/recipes/index.md - Refactor / bugfix β the layer's rule file in
.brain/rules/index.md - Feature work β existing memo in
.brain/features/<slug>.md, plus past attempts in.brain/runs/
3. Verify β /verify-done (or .brain/recipes/99-verify-done.md)
- typecheck + test + e2e (if cross-component) + build (if CF-touching) + manual UI smoke (if UI)
- Brain coherence: every diffed path β owning brain doc updated
- Five non-negotiables grep-clean
- Close the run note if you opened one
Shipping a feature β /ship-feature
- Runs
/verify-done, thenbrain ship <slug> --evidence "..."(flipsfeature_list.jsonto"shipped", screenshot-check, checkpoint,brain check), updates per-feature MD changelog, closes run note. Refuses on red.
The five non-negotiables (also in .brain/codebase/effect-ts.md):
- Effect TS by default. No
throw. Notry/catchoutsideEffect.tryPromise. - Effect Schema for validation. No Zod.
- Tagged errors only. All in
app/models/errors/. Map every one inapp/lib/effect-trpc.ts. - Unit test every helper, repository, and service. See
.brain/codebase/testing.md. - Cloudflare Workers, not Node. Bindings via the
CloudflareEnvTag. Neverprocess.env.
This repo follows the walkinglabs 5-subsystem harness framework. The harness is everything that keeps coding agents reliable across sessions: instructions, state, verification, scope, and lifecycle.
The .brain/ harness is driven by the brain-axi CLI (brain) β it reads/writes brain state (features, checkpoints, docs, runs, verifications) with token-efficient TOON output, so agents prefer it over hand-editing files. Install: npx skills add SeanningTatum/brain-axi --skill brain.
Single explainer: .brain/HARNESS.md. The 5 subsystems mapped to actual files (and the brain commands that drive them):
| Subsystem | Artifacts |
|---|---|
| Instructions | CLAUDE.md / AGENTS.md, .brain/ (rules, recipes, features) β via brain docs / brain search |
| State | .brain/features/feature_list.json (via brain features / set-status / ship), .brain/runs/progress.md (via brain progress), per-task run notes (via brain runs) |
| Verification | .brain/recipes/99-verify-done.md, /verify-done, brain check + scripts/harness-check.sh, brain playbook verify |
| Scope | One in-progress feature at a time (enforced by brain check + harness-check.sh); β€2 features per diff |
| Lifecycle | init.sh, .claude/hooks/session-start.sh (SessionStart hook), .claude/hooks/brain-reminder.sh (PreToolUse hook) |
| Command | What it does |
|---|---|
/start-task |
Kickoff: init.sh --baseline + brain read + framing + run note + progress entry. Refuses if scope policy violated. |
/verify-done |
Full verification: typecheck/test/e2e/build/UI/brain coherence/non-negotiables + brain check. |
/ship-feature |
Close out: /verify-done + brain ship <slug> --evidence (flip + checkpoint + brain check) + update feature MD + close run note. |
/harness-check |
brain check (brain-state invariants) + repo supplement (sync rule, sub-agent frontmatter, dead links) via scripts/harness-check.sh β no LLM, exits non-zero on drift. |
scripts/harness-check.sh wraps brain check and adds the repo-only invariants; run it (or init.sh) yourself anytime to verify state without invoking Claude. Requires the brain CLI on PATH (npm i -g github:SeanningTatum/brain-axi#v0.1.0).
Project-local sub-agents (.claude/agents/)
Five subagents wrap specific harness pieces. The main thread delegates to these so it doesn't have to re-load harness rules every turn.
| Agent | Use when |
|---|---|
brain-navigator |
Before writing code β get the reading list for a task |
recipe-runner |
Adding code that matches one of the 8 add-* recipes |
effect-ts-enforcer |
After writing β review diff against the 5 non-negotiables |
verify-done-runner |
Before declaring done β runs the full verification checklist |
feature-tracker |
Status changes (start / ship / block / scope a feature) |
See .claude/agents/README.md for invocation flow + complementary plugin agents.
Retrieval-over-recall. Each subdirectory has an index.md that signals "read me when X". Don't open files at random β start at the index.
.brain/
βββ HARNESS.md The harness, explained β single overview of the 5 subsystems
βββ high-level-architecture/ System layers, data flow, security, integrations
βββ codebase/ Effect TS programming model, helpers, tests, i18n, tRPC API
βββ rules/ 7 layer-aligned rules (frontend / cloudflare / repository /
β services / routes / library / errors)
βββ recipes/ Paste-able runbooks (00-before-task / 99-verify-done bookends + add-*)
βββ runs/ progress.md (rolling cursor) + per-task <date>-<slug>.md work logs
βββ features/ Per-feature memory + feature_list.json (machine-readable status)
βββ transcripts/ Meeting notes / decision logs (the "why")
βββ emails/ Stakeholder correspondence
βββ CHANGELOG.md Architectural + brain shifts (not a code changelog)
| Task | Open |
|---|---|
| Adding any code | recipes/index.md β pick a recipe |
| Editing UI / forms / Tailwind | rules/frontend.md |
| Editing a repository | rules/repository.md |
| Editing a service | rules/services.md |
| Editing a tRPC route or page loader | rules/routes.md |
| Editing wrangler / bindings / Workflows | rules/cloudflare.md |
| Adding a tagged error | rules/errors.md + recipes/add-tagged-error.md |
| Helpers / Effect Schema / tests | rules/library.md |
| Designing a feature | features/index.md β _TEMPLATE.md β existing examples |
| Tracing a constraint with no obvious "why" | transcripts/ and emails/ |
Each recipe is a deterministic checklist with code snippets, brain-doc updates, and a "definition of done". Designed for one-shot agent execution.
| Recipe | When |
|---|---|
00-before-task.md |
Bookend. Run at the start of every non-trivial task |
99-verify-done.md |
Bookend. Run before declaring a task done. Also /verify-done |
add-trpc-endpoint.md |
New tRPC procedure (query/mutation) |
add-db-table.md |
New D1 table + Drizzle schema + repository |
add-tagged-error.md |
New domain error + tagToTRPC mapping |
add-cf-binding.md |
New Cloudflare binding (KV, DO, Queue, Vectorize, etc.) |
add-service.md |
New Effect service (external client, lifecycle-bearing) |
add-route.md |
New React Router page (loader / action / UI / i18n) |
add-feature.md |
End-to-end feature combining the above |
recipes/index.md also has decision trees (e.g. "tRPC vs Workflow vs Queue?", "D1 vs R2 vs KV?") to disambiguate before you start.
These run automatically β you do not need to remember them.
Pre-commit gate (.githooks/pre-commit)
Triggered on git commit. Skips entirely if no .ts/.tsx/.js/.jsx files are staged. Otherwise:
bun run typecheck # cf-typegen + react-router typegen + tsc -b
bun run test # vitest run (123+ unit tests)Auto-installed via postinstall (sets core.hooksPath = .githooks). Re-install manually with bun run hooks:install. Bypass for an emergency commit:
SKIP_HOOKS=1 git commit -m "..."Brain reminder (.claude/hooks/brain-reminder.sh)
Fires from Claude Code's PreToolUse hook on git commit*. Looks at staged paths and prints which .brain/ docs likely need updating. Cheap shell script β no LLM call, never blocks.
Example output:
π§ Brain-update reminder (commit not blocked):
β’ app/db/schema.ts β .brain/high-level-architecture/data-models.md + .brain/codebase/api.md
β’ app/repositories/ β .brain/rules/repository.md
Both files are byte-identical by design β CLAUDE.md for Claude Code, AGENTS.md for AGENTS-spec tools (Codex, Aider). When you edit one, mirror to the other:
cp CLAUDE.md AGENTS.md(There's no CI check yet β that's tracked.)
app/
βββ auth/ Better Auth server config + client
βββ components/ UI β shadcn primitives in ui/, feature components alongside
βββ db/ Drizzle schema + connection
βββ hooks/ React hooks
βββ i18n/ SSR + client i18next setup, types
βββ lib/ Helpers β schemas/, logger, effect-trpc, effect-utils
βββ locales/en/ Translation JSON files (one per namespace)
βββ models/errors/ Tagged error classes
βββ repositories/ Drizzle-backed Effect.Service repositories
βββ routes/ React Router v7 file-based routes
βββ services/ Effect Tag/Layer services (Database, Bucket, AuthApi, etc.)
βββ trpc/ tRPC router + procedures + middleware
βββ routes.ts Route config
βββ runtime.ts ManagedRuntime composition (services + repos)
βββ root.tsx Root layout
βββ entry.{client,server}.tsx
.brain/ Agent-readable docs (see above) β includes HARNESS.md
.githooks/ Git hooks (pre-commit gate)
.claude/ Claude Code config β settings, hooks, agents/, commands/
drizzle/ SQL migrations
e2e/ Playwright tests
public/ Static assets
scripts/ Setup / teardown / seed + harness-check.sh
workers/app.ts Cloudflare Workers entrypoint
workflows/ Cloudflare Workflow definitions
init.sh Harness bootstrap β install + migrate + typecheck + test
./init.sh # Harness bootstrap β install + migrate + typecheck + test
./init.sh --baseline # Baseline only (typecheck + test) β used by /start-task
./init.sh --quick # Skip install + migrate (assume already done)
./scripts/harness-check.sh # 10 deterministic harness invariants (also: /harness-check)
bun run dev # Dev server (auto-runs local DB migrations) β :5173
bun run build # Production build
bun run deploy # Build + deploy to Cloudflare Workers
bun run preview # Build + serve locally via the Cloudflare Vite plugin (vite preview)
bun run typecheck # cf-typegen + react-router typegen + tsc -b
bun run test # Vitest (one-shot)
bun run test:watch # Vitest watch
bun run test:e2e # Playwright
bun run test:e2e:ui # Playwright UI mode
bun run test:e2e:report # Open last Playwright report
bun run db:generate # Generate Drizzle migration from schema
bun run db:migrate:local # Apply migrations to local D1
bun run db:migrate:remote # Apply migrations to remote D1
bun run db:seed # Seed local D1 with admin/user/banned fixtures
bun run db:seed:preview # Seed the preview-env D1 with the same fixtures
bun run db:studio # Drizzle Studio (visual DB editor)
bun run cf-typegen # Regenerate worker-configuration.d.ts
bun run hooks:install # Re-install pre-commit gate
bun run setup # First-time wizard
bun run teardown # Tear down Cloudflare resourcesBetter Auth 1.4 with the Drizzle adapter and admin() plugin for RBAC.
- Auth handler:
/api/auth/*(app/routes/api/auth.$.ts) - Server config:
app/auth/server.ts - Client:
app/auth/client.ts(usesadminClient()plugin) - Session reading in loaders:
context.auth.api.getSession({ headers: request.headers }) - Session reading in Effects: yield the
SessionTag fromapp/services/session.ts
RBAC β admin role enforcement is two-layered:
- Page-level β admin route loaders redirect non-admins (see
app/routes/admin/_layout.tsx) - Procedure-level β
adminProceduremiddleware inapp/trpc/index.tsrejects non-admins server-side
Never trust the page guard alone β UI hiding doesn't equal authorization.
Set the production secret:
openssl rand -base64 32
wrangler secret put BETTER_AUTH_SECRETPromote a user to admin β Drizzle Studio:
bun run db:studio
# Open user table β edit `role` cell β change "user" β "admin" β saveOr remote:
bunx wrangler d1 execute YOUR_DB_NAME --remote \
--command "UPDATE user SET role = 'admin' WHERE email = 'user@example.com'"Or seed the fixture accounts locally:
bun run db:seedGives you admin@preview.local / Password123! (plus user@preview.local
and banned@preview.local). See Database below and
.brain/rules/repository.md ("Seed data").
D1 (SQLite) accessed through Drizzle. Schema lives in app/db/schema.ts β single file at current scale.
Workflow for schema changes:
# 1. Edit app/db/schema.ts
# 2. Generate migration
bun run db:generate
# 3. Review SQL in drizzle/<NNNN>_<name>.sql
# 4. Apply locally
bun run db:migrate:local
# 5. After PR merge, apply to prod
bun run db:migrate:remoteUse SQL defaults for timestamps (
unixepoch('subsecond') * 1000), not JS-sidenew Date(). Seerecipes/add-db-table.md.
ShadCN primitives in app/components/ui/. Compose into feature components elsewhere. Tailwind v4 with oklch CSS variables in app/app.css.
Add a shadcn component:
bunx shadcn@latest add <component-name>Forms β always Effect Schema + effectResolver (no Zod resolver):
import { effectResolver } from "@hookform/resolvers/effect-ts";
import { LoginSchema, type LoginInput } from "@/lib/schemas/auth";
const form = useForm<LoginInput>({
resolver: effectResolver(LoginSchema),
});Dark mode is wired via next-themes (attribute="class", defaultTheme="system").
i18n β every route declares its namespaces and uses the matching useTranslation():
export const handle = { i18n: ["dashboard", "common"] };
// Inside the component:
const { t } = useTranslation("dashboard");Strings live in app/locales/en/<namespace>.json. Types in app/i18n/i18n.d.ts.
bun run deploy # Build + deploy to production
bunx wrangler versions deploy # Promote an uploaded version to productionObservability is enabled in wrangler.jsonc (logs, 100% head sampling). Smart placement is on.
Every pull request can get its own live preview, isolated from production.
How it works β wrangler.jsonc declares a preview wrangler environment: its own Worker (<name>-preview), a shared preview R2 bucket, and a D1 binding that CI repoints at a per-PR database. .github/workflows/preview.yml builds against that environment and uploads a Worker version tagged with a stable alias, pr-<N>, so the preview URL for a given PR doesn't change across pushes:
https://pr-<N>-<worker>-preview.<subdomain>.workers.dev
The Action also comments the URL (and a per-commit version URL) on the PR.
Data isolation β each PR gets its own D1 database (<name>-db-pr-<N>). On PR open (and every push), scripts/ci/setup-preview-db.ts creates the database if needed and patches the CI checkout's wrangler.jsonc to point the preview DATABASE binding at it (the patch is never committed); CI then applies migrations and seeds fixtures (scripts/seed-preview.ts --preview) before uploading the version, so every preview URL has representative, logged-in-ready data from the first push. On PR close, .github/workflows/preview-cleanup.yml deletes that database. The preview R2 bucket is shared across all PR previews β not per-PR, because wrangler can't delete non-empty buckets, so per-PR buckets would accumulate as orphans. bun run teardown also sweeps up any leftover -db-pr-* databases.
Test accounts β every preview and local D1 gets the same three fixtures (bun run db:seed / bun run db:seed:preview, password Password123! for all):
| Role | Notes | |
|---|---|---|
admin@preview.local |
admin | admin dashboard access |
user@preview.local |
user | regular account |
banned@preview.local |
user | banned = true, for testing the ban UI |
Seeding is idempotent (INSERT OR IGNORE on fixed seed-* ids) β safe to rerun on every PR synchronize. See .brain/rules/repository.md ("Seed data") for the rule that new tables/features must extend these fixtures.
Free plan note: Cloudflare's D1 free plan caps you at 10 databases, so per-PR databases assume a paid plan. On the free plan, revert the "Provision per-PR D1" step in
preview.ymlback to migrating the shared<name>-db-previewdatabase (a one-line change).
One-time enablement for your repo β previews ship disabled. To turn them on:
- Set the repo variable
CLOUDFLARE_ACCOUNT_ID(Settings β Secrets and variables β Actions β Variables tab). - Set the repo secret
CLOUDFLARE_API_TOKEN(same page β Secrets tab), created from the "Edit Cloudflare Workers" API token template plusD1:Editadded (migrations need it). bun run setupalready provisions thepreviewWorker/R2 resources (plus the shared-db-previewD1 used for manual preview work) alongside production; per-PR D1 databases are created automatically by CI β there's nothing else to create.
Manual commands β useful for a one-off preview without opening a PR:
bun run deploy:preview # Build + deploy the preview Worker directly
bun run db:migrate:preview # Apply migrations to the preview D1
CLOUDFLARE_ENV=preview bun run build \
&& bunx wrangler versions upload --preview-alias my-branch # Build + upload a one-off preview URLGotcha:
wrangler deploy --env previewdoes not work with the Cloudflare Vite plugin. The plugin selects the environment at build time via theCLOUDFLARE_ENVenv var and writes a redirected config tobuild/server/wrangler.jsonthat has noenvblock β so any--envflag passed to a post-buildwranglercommand has nothing to select. Always setCLOUDFLARE_ENVbefore building instead (asdeploy:previewand the commands above do).
- VS Code / Cursor β uses the local TypeScript server. The
typescript-lspClaude plugin (auto-enabled in.claude/settings.json) gives agents diagnostics inline. - MCP servers β optional. If you want richer tool access (Tavily search, Playwright control, etc.), set them up via Claude Code's
/pluginsystem instead of repo-local config.
- Branch off
main - Read the relevant
.brain/recipes/runbook before starting - Pre-commit gate runs typecheck + tests automatically
- Update the
.brain/doc that owns your change (the brain-reminder hook will tell you which) - Append a one-line entry to
.brain/CHANGELOG.md - Open the PR
When in doubt: read first, code second.