cf-saas-starter-react-router

agent
Security Audit
Fail
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.

SUMMARY

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.

README.md

cf-saas-starter-react-router

Release
Cloudflare Workers
React Router
Effect TS
TypeScript

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.

The Harness Loop — /start-task → work → /verify-done → /ship-feature, gated by typecheck, test, and brain-sync
Every task runs the same loop. Deterministic gates keep the agent coherent across sessions.

Three pillars:

  1. Harness-first — agents stay coherent across sessions via the 5-subsystem framework: instructions, state, verification, scope, lifecycle.
  2. AI-first — five project-local sub-agents, eight paste-able recipes, four deterministic slash commands, five grep-checkable non-negotiables.
  3. DX that just works — bun install + bun run dev and you're live. bun run scripts/first-time-setup.ts gets 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.TaggedError mapped to tRPC codes via tagToTRPC
  • 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 by 00-before-task.md (init) and 99-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-done slash command runs the full termination checklist
  • CLAUDE.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 --baseline to 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 brain CLI (brain, brain progress, brain docs, brain search)
  • Validates scope policy via brain features --status in-progress (brain check enforces 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

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, then brain ship <slug> --evidence "..." (flips feature_list.json to "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):

  1. Effect TS by default. No throw. No try/catch outside Effect.tryPromise.
  2. Effect Schema for validation. No Zod.
  3. Tagged errors only. All in app/models/errors/. Map every one in app/lib/effect-trpc.ts.
  4. Unit test every helper, repository, and service. See .brain/codebase/testing.md.
  5. Cloudflare Workers, not Node. Bindings via the CloudflareEnv Tag. Never process.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.

RBAC — admin role enforcement is two-layered:

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-side new Date(). See recipes/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):

Email 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.yml back to migrating the shared <name>-db-preview database (a one-line change).

One-time enablement for your repo — previews ship disabled. To turn them on:

  1. Set the repo variable CLOUDFLARE_ACCOUNT_ID (Settings → Secrets and variables → Actions → Variables tab).
  2. Set the repo secret CLOUDFLARE_API_TOKEN (same page → Secrets tab), created from the "Edit Cloudflare Workers" API token template plus D1:Edit added (migrations need it).
  3. bun run setup already provisions the preview Worker/R2 resources (plus the shared -db-preview D1 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 preview does not work with the Cloudflare Vite plugin. The plugin selects the environment at build time via the CLOUDFLARE_ENV env var and writes a redirected config to build/server/wrangler.json that has no env block — so any --env flag passed to a post-build wrangler command has nothing to select. Always set CLOUDFLARE_ENV before building instead (as deploy:preview and the commands above do).


Editor integrations

  • VS Code / Cursor — uses the local TypeScript server. The typescript-lsp Claude 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 /plugin system instead of repo-local config.

Contributing

  1. Branch off main
  2. Read the relevant .brain/recipes/ runbook before starting
  3. Pre-commit gate runs typecheck + tests automatically
  4. Update the .brain/ doc that owns your change (the brain-reminder hook will tell you which)
  5. Append a one-line entry to .brain/CHANGELOG.md
  6. Open the PR

When in doubt: read first, code second.

Reviews (0)

No results found