cf-saas-starter-react-router
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Fail
- rm -rf — Recursive force deletion command in .claude/hooks/rule-router.sh
- rm -rf — Recursive force deletion command in .claude/settings.json
- process.env — Environment variable access in .claude/workflows/build-feature.js
- fs module — File system access in .github/workflows/preview-cleanup.yml
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Harness-first, AI-first SaaS starter. Cloudflare Workers + React Router v7 + tRPC + D1 + Drizzle + Better Auth + Effect TS. Same runtime locally and in prod. Ship features in minutes, not weeks.
cf-saas-starter-react-router
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.
What's in the box
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
In the wild
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.
Quick Start
Prerequisites
# Bun (package manager + runtime)
curl -fsSL https://bun.sh/install | bash
# Cloudflare CLI
bun add -g wrangler
wrangler login
Option A — Automated setup (recommended)
bun run scripts/first-time-setup.ts
The 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.
Option B — Manual
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:5173
How To Work In This Repo
For humans
Read CLAUDE.md once. It points to everything else.
For agents
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/ - Frontend work → load the design skill for the lane (see Design skills below)
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.
Harness — the system around the agent
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) |
Slash commands (deterministic gates)
| 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.
The .brain/ directory
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)
When to read what
| 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/ |
Recipes — paste-able runbooks
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.
Guardrails
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
CLAUDE.md ↔ AGENTS.md sync
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.)
Project layout
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
Commands
./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 resources
Authentication
Better 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_SECRET
Promote a user to admin — Drizzle Studio:
bun run db:studio
# Open user table → edit `role` cell → change "user" → "admin" → save
Or remote:
bunx wrangler d1 execute YOUR_DB_NAME --remote \
--command "UPDATE user SET role = 'admin' WHERE email = '[email protected]'"
Or seed the fixture accounts locally:
bun run db:seed
Gives you [email protected] / Password123! (plus [email protected]
and [email protected]). See Database below and.brain/rules/repository.md ("Seed data").
Database
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:remote
Use SQL defaults for timestamps (
unixepoch('subsecond') * 1000), not JS-sidenew Date(). Seerecipes/add-db-table.md.
UI / Design system
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>
Design skills
Agents load one of these on every frontend task. skills-lock.json lists them, and scripts/restore-skills.sh installs any that are missing — run by ./init.sh, and in the background by the Claude Code SessionStart hook — so a fresh clone has them by its first session. They land in .agents/skills/ (gitignored); the committed .claude/skills/ symlinks point there.
| Skill | Source | Job here |
|---|---|---|
impeccable |
pbakaus/impeccable | Leads app screens — dashboard, admin, settings, forms. Runs the finish pass (audit, harden, polish) on every surface. |
design-taste-frontend |
Leonxlnx/taste-skill | Leads public pages — landing, marketing, pricing. Art direction plus a screenshot look-and-fix loop. |
emil-design-eng · animate · review-animations |
emilkowalski/skills | Own all motion — easing, duration, enter/exit, gestures. review-animations runs only when you call /review-animations. |
One skill leads each surface, so they never fight over the same choice. The tokens in app/app.css beat all of them. Lane rules: .brain/rules/frontend.md.
The restore pulls each skill's latest upstream version, unpinned.
./scripts/restore-skills.sh # install missing skills now
npx skills add <owner/repo> --skill <name> -y # add another, then:
ln -s ../../.agents/skills/<name> .claude/skills/<name> # expose it to Claude Code; commit lock + symlink
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.
Deployment
bun run deploy # Build + deploy to production
bunx wrangler versions deploy # Promote an uploaded version to production
Observability is enabled in wrangler.jsonc (logs, 100% head sampling). Smart placement is on.
Preview deployments (per-PR)
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 | |
|---|---|---|
[email protected] |
admin | admin dashboard access |
[email protected] |
user | regular account |
[email protected] |
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 URL
Gotcha:
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).
Editor integrations
- 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.
Contributing
- 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.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found