Skip to content

SeanningTatum/cf-saas-starter-react-router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

45 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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.

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.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.


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

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 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)
  • Validates scope policy via jq on feature_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

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, flips feature_list.json status 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):

  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.

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)

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.
/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.


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 "..."

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 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 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 = 'user@example.com'"

Or seed a test admin locally:

bun run scripts/seed-test-admin.ts

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>

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 upload     # Deploy a preview URL
bunx wrangler versions deploy     # Promote a preview to production

Observability is enabled in wrangler.jsonc (logs, 100% head sampling). Smart placement is on.


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.