🏛️ starter is an opinionated monorepo template for serious TypeScript with Effect and AI coding agents. 🔒 One architecture, zero knobs, and mechanical gates that reject the slop agents produce when left unconstrained. 🚀 Built for engineers accountable for codebases where AI writes the commits.
AI coding agents produce TypeScript that compiles cleanly and passes shallow unit tests while quietly violating foundational architecture: ambient side effects inside decision logic, unchecked type assertions (as Type), and mock-heavy test suites that mask runtime breakage.
starter establishes an uncompromising substrate. Invariants are not aspirational guidelines or doc comments; they are enforced mechanically by linter rules, complexity ceilings, mutation test floors, and continuous integration gates.
| Concern | The Naive AI-Assisted Default | The Endgame Architecture (starter) |
|---|---|---|
| Domain Logic | ❌ Interleaved I/O, clocks, random generators, and mutations | ✅ Pure functions returning tagged Decision or Refusal unions |
| Branching | ❌ Sprawling nested if/else and procedural loops |
✅ Cyclomatic complexity 1 via exhaustive pattern matching (Match) |
| Boundary Data | ❌ Unchecked casts (as unknown as Type, @ts-ignore) |
✅ Strict Schema.decode transforming raw bytes into branded types |
| Test Verification | ❌ Mock-heavy tests pinning internal implementation | ✅ 100% mutation kill floor (Stryker) and property-based tests |
| Configuration | ❌ Dozens of toggles that let agents bypass strictness | ✅ Zero knobs — one proven opinionated toolchain end to end |
| Package Entries | ❌ Star exports (export *) hiding dependency graphs |
✅ Explicit named re-exports enumerated one line per symbol |
| Refactoring | ❌ Patching around rotten legacy modules | ✅ Delete-first rebuild with published observable pinning |
Every external interaction in a starter project follows the I/O Sandwich:
read (impure) ──► decode (pure) ──► decide (pure) ──► shape (pure) ──► write (impure)
- 📥
read— Gathers raw input from ports and external systems. - 🔍
decode— Validates unvalidated input into branded domain types using Schema. - 🧠
decide— Executes domain logic with cyclomatic complexity 1 (zero I/O, zero ambient state). - 📦
shape— Builds pure output documents and domain events from the decision. - 📤
write— Persists changes, emits domain events, or returns responses.
starter wires a modern, fast, and type-safe toolchain across the workspace:
| Tool | Role & Configuration |
|---|---|
| ⚡ pnpm Workspaces | Strict workspace dependency management with catalog versioning (pnpm-workspace.yaml) |
| 🏎️ Turbo | High-performance task pipeline with cached builds, tests, and lint runs |
| 🛡️ Effect 4 | The standard functional effect system |
| 🔍 oxlint | Rust-based linter enforcing strict TypeScript rules and the house presets |
| 🎨 dprint | Fast, deterministic code and markdown formatting (dprint.json) |
| 🧪 Vitest | Fast unit and integration test runner with TypeScript support |
| 🔬 Stryker | Mutation testing ensuring tests fail when bugs are introduced |
| 📝 Changesets | Automated versioning and changelogs, released as git tags via shared tooling |
| 🪝 Husky & Commitlint | Git hooks enforcing conventional commit standards |
| 🌳 Worktrunk Scripts | Deno-powered git worktree lifecycle hooks for isolated agent work |
The repository is structured into two workspace roots defined in pnpm-workspace.yaml:
.
├── packages/ # Reusable libraries, engines, and domain cores
├── apps/ # The Worker and its end-to-end journeys
├── repos/ # Vendored subtrees (constitution, worktrunk-scripts)
└── docs/ # Solutions, tooling decisions, and plans
apps/site— The one Cloudflare Worker: a TanStack Start site that calls its Worker through effect/rpc (one RpcGroup served at/api/rpc, a typed RpcClient in the page) over one D1 database, defined and deployed with Alchemy.pnpm devemulates the database locally; thehealthprocedure probes the database and answersokonly when the pure decisioncheck-health.workflow.tsfinds the probe answered, refusing withDatabaseUnreachableotherwise.apps/site-e2e— The end-to-end journeys that run againstpnpm dev(pnpm journeys).
Click the Use this template button on GitHub, or create a repository via the GitHub CLI:
gh repo create my-effect-project --template systemfsoftware/starter --public
cd my-effect-projectInstalls, builds, tests and git hooks run their dependency code inside a deny-by-default sandbox, never directly on your machine. The Nix dev shell pins the toolchain and carries the sandbox (prerequisites):
nix develop # or: direnv allow
pnpm bootstrappnpm build
pnpm check:cipnpm dev # the whole app at http://localhost:1337, Alchemy's local emulation, no cloud
pnpm journeys # the end-to-end journeys in a real browser against pnpm devThe site ships one example feature, a guestbook at /guestbook: a pure decision (sign-guestbook.workflow.ts, built with Workflow.make) trims a name and a message and refuses them with typed errors; the RPC procedures sign and list write and read entries in D1, with the decision's tagged refusals as sign's error schema; and the page shows the entries or the refusal. Everything it owns lives in apps/site/src/features/guestbook and apps/site-e2e/tests/features/guestbook.
To remove it:
- Delete
apps/site/src/features/guestbook. - Delete
apps/site-e2e/tests/features/guestbook. - Delete the route file
apps/site/src/routes/guestbook.tsx. apps/site/src/api/site-rpcs.ts: drop theGuestbookRpcsimport and makeSiteRpcsjustHealthRpcs.apps/site/src/api/site-rpc-server.ts: drop theGuestbookHandlersimport and itsLayer.provide(GuestbookHandlers)line.apps/site/alchemy.run.ts: drop themigrationsoption fromCloudflare.D1.Database('Database', …).
Removal leaves a deployed D1 as it is: the guestbook_entries table and its 0001_create_guestbook_entries.sql row in __alchemy_migrations stay; drop them from the D1 console in the Cloudflare dashboard with DROP TABLE guestbook_entries; DELETE FROM __alchemy_migrations WHERE name = '0001_create_guestbook_entries.sql';.
A copy gets the template's files, not its repository settings, so nothing stops a pull request with failing checks from merging until main has a ruleset that requires them. Install the "gates" ruleset from .github/rulesets/gates.json once, with your own GitHub credentials:
gh api -X POST repos/{owner}/{repo}/rulesets --input .github/rulesets/gates.jsonUntil it is installed, CI's rules check fails on every pull request and every push to main, and the production deploy waits on it.
Your copy deploys to your own Cloudflare account; the template holds no credentials. Set CLOUDFLARE_API_TOKEN (a token that can edit Workers and D1) and CLOUDFLARE_ACCOUNT_ID, and optionally SITE_DOMAIN (a hostname in a zone on that account) to serve production there instead of on workers.dev:
pnpm run deploy # stage prod
ALCHEMY_STAGE=pr-12 pnpm run deploy # a preview stage
ALCHEMY_STAGE=pr-12 pnpm run destroypnpm run deploy, not pnpm deploy: the bare form is pnpm's own built-in command.
In your copy, with the two secrets set as repository secrets (and SITE_DOMAIN as a repository variable), every same-repo pull request gets a preview with its URL in a comment, and main deploys production once the release gate passes. The template itself never deploys.
All changes must satisfy local and continuous integration verification gates:
# Format code and markdown
pnpm format:check
# Typecheck workspace packages
pnpm typecheck
# Run linter across packages
pnpm lint
# Run unit and integration tests
pnpm test
# Run full CI suite locally
pnpm check:ciWhy does starter require Effect 4 instead of Effect 3?
Effect 4 brings the schema transformations and typed services the template's decisions and boundaries are written with. starter targets the future of Effect rather than supporting legacy patterns.
Why are there no configuration options or preset levels?
Every configuration toggle provides a route for AI agents to downgrade verification standards and reintroduce slop. Zero knobs guarantees that all packages created from this template adhere to identical architectural standards.
How does mutation testing work in this template?
Stryker introduces deliberate syntax and logic mutations into your code and runs your test suite against each mutant. If your tests still pass when code behavior changes, the mutant survives and the gate fails. Domain decisions require a 100% kill score. Mutation runs only in the release gate on pushes to main, never locally or on pull requests.
How do I migrate an existing codebase to this architecture?
Follow the strangler pattern: pin the published observable behavior of a module, delete the legacy file completely, and rebuild it from a blank page using pure I/O sandwiches. Never patch around a flawed core.
Development setup, workflows, and PR guidelines are documented in CONTRIBUTING.md.
Licensed under the Apache-2.0 License.