New
Apps 1,895Makers 1,890This week 1,685
Promote your app−30%
0views in
the last 30 days

CF SaaS Starter

SaaS starter on Cloudflare Workers, React Router v7 and tRPC

Built with Claude Code

by seanningtatum · Unclaimed · from CACanada · added Oct 29, 2026
no ratings0 comments

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 with specs and tickets in GitHub issues, planning skills, 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 — six project-local sub-agents, Matt Pocock's planning skills, seven gates, five AST-checked 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 how to read the project state 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

  • GitHub issues and PRs — specs, tickets and verdicts; the project state
  • scripts/verify.sh — runs the gates; CI runs the same ones
  • .githooks/pre-commit — typecheck + tests block broken commits (no-dep, auto-installs)
  • CLAUDE.md → AGENTS.md — one file (symlink), 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 plan will cost rework. Project state lives in GitHub issues and PRs, plus AGENTS.md, GLOSSARY.md and docs/adr/. The steps:

  1. Start. Read the issue (gh issue view), the docs above, and the open PRs. Run ./scripts/verify.sh --baseline so you know the tree is green before you touch it.
  2. Plan when the task carries a decision. /grill-me interviews you until the design is clear; /grill-with-docs does the same and also writes GLOSSARY.md and ADRs. /to-spec writes the spec as a GitHub issue. /to-tickets cuts it into vertical-slice issues with blocking links.
  3. Work. One ticket at a time. Frontend work loads the design skill for its lane (see Design skills below).
  4. Verify. The feature-verifier sub-agent walks the flow in a browser, saves screenshots to .verify/<slug>/, and writes a PASS / FAIL / BLOCKED verdict you paste into the PR.
  5. Done. ./scripts/verify.sh is green and the verdict is PASS. Merge the PR that closes the issue.

The five non-negotiables (enforced by the non-negotiables gate, scripts/check-non-negotiables.ts):

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

State lives in GitHub: the spec is an issue, each ticket is an issue, and a merged PR ships it. Nothing sits in a local database, so a fresh clone sees the same plan. Lasting rules go in AGENTS.md, GLOSSARY.md or an ADR in docs/adr/. Planning uses Matt Pocock's skills (installed from skills-lock.json); no extra setup beyond gh signed in.

Subsystem Artifacts
Instructions CLAUDE.md / AGENTS.md, Matt Pocock's skills, GLOSSARY.md, docs/adr/
State GitHub issues (spec and tickets) and PRs (verdicts); gh issue list, gh pr list
Verification Gates run by scripts/verify.sh; scripts/harness-check.sh; feature-verifier browser walks
Scope One ticket at a time; ≤2 features per diff
Lifecycle init.sh, .claude/hooks/session-start.sh (skill restore + pointers)

Gates

Gate Command Runs in
typecheck bun run typecheck baseline, full
test bun run test baseline, full
non-negotiables-selftest bun run scripts/check-non-negotiables.ts --selftest baseline, full
non-negotiables bun run scripts/check-non-negotiables.ts baseline, full
harness ./scripts/harness-check.sh baseline, full
build bun run build full
e2e bun run test:e2e full

./scripts/verify.sh --baseline runs the quick set; bare ./scripts/verify.sh runs them all. CI runs the same script, so adding a gate is a one-line change there.

Slash commands

Command What it does
/harness-check Repo invariants (CLAUDE↔AGENTS symlink, sub-agent frontmatter, hook wiring) via scripts/harness-check.sh — no LLM, exits non-zero on drift.

Project-local sub-agents (.claude/agents/)

Agent Use when
effect-ts-enforcer After writing — review the diff against the 5 non-negotiables
test-author After a change — write the tests that pin it
span-instrumenter After adding a procedure, repo method or service — add tracing spans
ui-builder Any user-visible work
context-memory Keep per-feature working memory — context, decisions, links, todos — shared by every worktree
feature-verifier Walk a user-visible flow in the browser and record the verdict

See .claude/agents/README.md for invocation flow + complementary plugin agents.


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

CLAUDE.md ↔ AGENTS.md

CLAUDE.md is a symlink to AGENTS.md, so both names resolve to one file. Edit AGENTS.md; scripts/harness-check.sh fails if the two ever differ.


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
.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 + verify.sh + harness-check.sh
workers/app.ts        Cloudflare Workers entrypoint
workflows/            Cloudflare Workflow definitions
init.sh               Harness bootstrap — install + skills + migrate + typecheck + test

Commands

./init.sh                     # Harness bootstrap — install + skills + migrate + typecheck + test
./init.sh --baseline          # Baseline only (typecheck + test + harness-check)
./init.sh --quick             # Skip install + migrate (assume already done)
./scripts/harness-check.sh    # repo 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:videos --out <dir>  # Record the @golden/@error specs as PR videos
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 = 'user@example.com'"

Or seed the fixture accounts locally:

bun run db:seed

Gives you admin@preview.local / Password123! (plus user@preview.local and banned@preview.local). See Database below. Every feature that adds a table or user-visible data extends these fixtures in the same diff.


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


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. None is committed; each comes from its upstream repo:

  • Plugins, enabled in .claude/settings.json: impeccable and taste-skill. Claude Code asks to install them when you trust the repo.
  • Plain skills, listed in skills-lock.json because their repo ships no plugin: the three motion skills. scripts/restore-skills.sh installs any that are missing into .agents/skills/ and links them into .claude/skills/ — run by ./init.sh, and in the background by the Claude Code SessionStart hook. Both directories are gitignored.
Skill Source Job here
impeccable (plugin) pbakaus/impeccable Leads app screens — dashboard, admin, settings, forms. Critiques app screens from screenshots (critique, P0/P1 block) and runs the finish pass (audit, harden, polish) on every surface.
design-taste-frontend (plugin taste-skill) Leonxlnx/taste-skill Leads public pages — landing, marketing, pricing. Art direction plus a screenshot look-and-fix loop that scores the render and gates public pages.
emil-design-eng · animate · review-animations (lock) 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: AGENTS.md.

The restore pulls each skill's latest upstream version, unpinned.

To add a design skill: if its repo ships .claude-plugin/marketplace.json, add the marketplace to extraKnownMarketplaces and the plugin to enabledPlugins in .claude/settings.json. Otherwise:

npx skills add <owner/repo> --skill <name> -y   # adds it to skills-lock.json
./scripts/restore-skills.sh                     # installs and links it; commit the lock only

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(Login

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#saas · #starter · #cloudflare