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.
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.0.0 β The Agent-First SaaS Starter
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
# 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 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)
- Validates scope policy via
jqonfeature_list.jsonβ 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, flipsfeature_list.jsonstatus to"shipped", updates per-feature MD changelog, closes run note, runs/harness-check. 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.
Single explainer: .brain/HARNESS.md. The 5 subsystems mapped to actual files:
| Subsystem | Artifacts |
|---|---|
| Instructions | CLAUDE.md / AGENTS.md, .brain/ (rules, recipes, features) |
| State | .brain/features/feature_list.json (machine-readable status), .brain/runs/progress.md (rolling cursor), per-task .brain/runs/<date>-<slug>.md |
| Verification | .brain/recipes/99-verify-done.md, /verify-done, scripts/harness-check.sh |
| Scope | One in-progress feature at a time (enforced by feature_list.json + 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. |
/ship-feature |
Close out: /verify-done + flip feature_list.json + update feature MD + close run note + /harness-check. |
/harness-check |
10 deterministic invariants via scripts/harness-check.sh (no LLM, exits non-zero on drift). |
The two scripts (init.sh + scripts/harness-check.sh) are pure shell β run them yourself anytime to verify state without invoking Claude.
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 via wrangler
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: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 a test admin locally:
bun run scripts/seed-test-admin.tsD1 (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 upload # Deploy a preview URL
bunx wrangler versions deploy # Promote a preview to productionObservability is enabled in wrangler.jsonc (logs, 100% head sampling). Smart placement is on.
- 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.