# Enforcement

Which rule is checked by what: type checks, lint, tests, build and deploy gates, scripts, and the rules that still rely on review.

A rule that nothing checks decays. This page lists the repository's rules, where each one is stated and what fails when it is broken. Use it to decide how much to trust a rule, and to find the next rule that should get a check.

## Levels [#levels]

| Level | Meaning |
| --- | --- |
| `[type]` | `pnpm check-types` rejects the violation. |
| `[lint]` | `pnpm lint` (oxlint plus repository checks) rejects it. |
| `[test]` | A test in `pnpm test`, the integration suite or the RLS matrix fails. |
| `[build]` | `pnpm --filter web build` fails. |
| `[deploy]` | A preview or production workflow refuses to publish. |
| `[tool]` | A repository script refuses the action. |
| `[manual]` | A verification script exists, but you run it yourself; CI does not. |
| `[convention]` | Only review holds it. These are the fragile ones. |

## Coverage [#coverage]

| Rule | Stated in | Enforcement |
| --- | --- | --- |
| Clients import leaves, contracts or types only | [ADR-0002](/docs/adr/0002-no-barrels-on-server-capable-modules) | `[lint]` `packages/config/oxlint-client-boundary.json` (server-only specifiers, including `@template-cloudflare/api/mcp/*`), extended by `apps/web/src`, `apps/mobile/src`, `packages/ui/src` · `[test]` `scripts/leaf-modules.test.mjs` (D1) · `[build]` `apps/web/scripts/assert-client-bundle-clean.mjs` |
| A feature contract carries the `LEAF MODULE` header | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[test]` `scripts/leaf-modules.test.mjs` |
| Package exports are enumerated and documented | [ADR-0002](/docs/adr/0002-no-barrels-on-server-capable-modules), [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[test]` `packages/api/src/exports.test.ts` · `[test]` the package-exports check in `apps/web/src/lib/docs/source.test.ts` |
| Clients import canonical API subpaths; no `./routers/*` subpath comes back | [ADR-0013](/docs/adr/0013-mobile-features-mirror-the-backend-shape) | `[test]` `packages/api/src/exports.test.ts` · `[type]` a removed subpath fails `tsc` |
| Mobile layers, cross-feature edges, platform variants, the route list and data access only in hooks | [ADR-0013](/docs/adr/0013-mobile-features-mirror-the-backend-shape) | `[test]` `scripts/mobile-feature-boundaries.test.mjs`, `scripts/import-cycles.test.mjs` (a test, not lint: see the note below) · `[lint]` `max-lines` in `apps/mobile/src/.oxlintrc.json` |
| Only the app router and the tool catalog import `*.procedures` (D3) | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[lint]` `.oxlintrc.json`, override for `packages/api/src/**` |
| Only the job runner imports `*.jobs` (D4) | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[lint]` `.oxlintrc.json`, override for `packages/api/src/**` |
| `platform/**` does not import features, except `admin.roles` and `admin.api` (D5) | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[lint]` `.oxlintrc.json`, override for `packages/api/src/platform/**` |
| Features import each other through public roles or declared edges (D6) | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[test]` `scripts/feature-boundaries.test.mjs` |
| No import cycles in `packages/api/src` and `packages/db/src` (D7) | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[test]` `scripts/import-cycles.test.mjs`, run by `pnpm test` |
| Migrated procedures hold no queries (D8) | [ADR-0011](/docs/adr/0011-backend-feature-shape) | `[lint]` `.oxlintrc.json`, override listing `todo` and `documents` procedures: refuses `drizzle-orm`, the schema barrel `@template-cloudflare/db/schema` and `@template-cloudflare/db/schema/<table>` · `[convention]` `context.db.query.*`, which needs no import |
| Tenant data only through `context.db` | [ADR-0009](/docs/adr/0009-row-level-security) | `[test]` `packages/api/src/platform/rpc/rls.isolation.test.ts`, `packages/db/src/rls-catalog.test.ts`, `test:rls-matrix` (CI job `rls-matrix`) |
| RLS survives restores and host moves | [ADR-0009](/docs/adr/0009-row-level-security) | `[deploy]` `db:verify-rls` at the end of `db:apply-pending` |
| Cross-tenant paths are listed | [ADR-0009](/docs/adr/0009-row-level-security) | `[lint]` `scripts/check-system-call-sites.mjs` against `packages/db/src/system-call-sites.txt`; pool-import restriction in `.oxlintrc.json` |
| No network I/O inside `withTenant` | `AGENTS.md` | `[convention]` |
| Isolation holds on a real deploy | [ADR-0009](/docs/adr/0009-row-level-security) | `[manual]` `scripts/rls-preview-proof.mjs` against the preview |
| Hyperdrive query caching stays off | [ADR-0009](/docs/adr/0009-row-level-security) | `[deploy]` `packages/project-setup-cli/src/hyperdrive.mjs check` in `.github/workflows/deploy-prod.yml` |
| `TENANCY_PROBE` only on previews | [ADR-0009](/docs/adr/0009-row-level-security) | `[deploy]` `scripts/check-production-config.mjs` |
| Domain failures are tagged and mapped once | [ADR-0010](/docs/adr/0010-procedures-run-one-effect-program) | `[type]` the `satisfies never` mapper in each `<feature>.errors.ts` · `[lint]` D2 in `.oxlintrc.json`: no `@orpc/*` in services, rules and policies of migrated features · `[test]` `packages/api/src/testing/characterization/*.client-errors.test.ts` |
| Every error code has a message in every language | [error handling](https://github.com/CarlosZiegler/template-cloudflare/blob/main/docs/error-handling.md) | `[type]` `satisfies` in `packages/errors/src/keys.ts` · `[test]` `packages/errors/src/locales.test.ts`, `apps/web/src/locales/locales.test.ts` |
| Effect never reaches a client | [ADR-0005](/docs/adr/0005-effect-boundary) | `[build]` `apps/web/scripts/assert-client-bundle-clean.mjs` |
| A procedure runs one program; a client abort after it starts does not roll back a write; database work drains | [ADR-0010](/docs/adr/0010-procedures-run-one-effect-program) | `[test]` `todo.abort.test.ts`, `documents.abort.test.ts`, `packages/api/src/platform/rpc/procedure-edge.test.ts`, `packages/api/src/platform/effect/db.test.ts` |
| One `run*` per entry point, no module-scope I/O runtime | [ADR-0005](/docs/adr/0005-effect-boundary), [ADR-0010](/docs/adr/0010-procedures-run-one-effect-program) | `[convention]` |
| Only idempotent reads retry | [ADR-0005](/docs/adr/0005-effect-boundary) | `[test]` `packages/api/src/platform/effect/retry.test.ts` · `[convention]` for new call sites |
| Deadlines use production values | [ADR-0005](/docs/adr/0005-effect-boundary) | `[test]` `@effect/vitest` with `TestClock`, e.g. `documents.jobs.test.ts`, `imports.jobs.test.ts` |
| Queue persistence changes pass the fault gate | [ADR-0006](/docs/adr/0006-effect-persisted-queue) | `[manual]` `scripts/test-persisted-queue-spike.mjs` |
| Configuration is declared in `packages/env` | [Backend conventions](/docs/development/backend-conventions) | `[lint]` `node/no-process-env` in `.oxlintrc.json` (`packages/**`) and `apps/web/src/.oxlintrc.json`, with named exceptions |
| Runtime switches fail safe on an invalid value | [Environment and secrets](/docs/development/environments#runtime-switches) | `[test]` `packages/env/src/runtime-flags.test.ts` |
| Every RPC response carries `x-request-id`, and the error log records the ID | [Error handling](https://github.com/CarlosZiegler/template-cloudflare/blob/main/docs/error-handling.md) | `[test]` `packages/api/src/platform/rpc/request-id.test.ts`, `apps/web/src/routes/api/rpc/$.test.ts` |
| Input validation answers HTTP 400 with `INPUT_VALIDATION` | [Error handling](https://github.com/CarlosZiegler/template-cloudflare/blob/main/docs/error-handling.md) | `[test]` `packages/errors/src/server.test.ts` (each `APP_ERRORS` status in `errorStatusMap`), `apps/web/src/routes/api/rpc/$.test.ts` (400 on the wire), characterization tests |
| Org role checks fail closed and only follow `orgProcedure` | [Backend conventions](/docs/development/backend-conventions) | `[type]` the middleware's input context requires `organizationRole` · `[test]` `packages/api/src/platform/rpc/require-org-role.test.ts` (including `@ts-expect-error` on other procedures) · `[convention]` where it is applied |
| Expected failures are not logged as errors | [Backend conventions](/docs/development/backend-conventions) | `[test]` `packages/errors/src/server.test.ts` |
| Schema changes are migrations, never pushes | [Database](/docs/features/database) | `[tool]` `db:push` runs `packages/db/scripts/refuse-push.mjs` · `[test]` CI applies migrations twice |
| Optional features can be removed cleanly | [Optional features](https://github.com/CarlosZiegler/template-cloudflare/blob/main/docs/optional-features.md) | `[manual]` `scripts/verify-optional-client-removal.mjs` |
| Dependencies respect the release quarantine and exact pins | [Library reference](/docs/libraries) | `[test]` `scripts/tooling-tests.mjs` |
| Formatting | [Testing and verification](/docs/development/testing) | `[lint]` `pnpm format:check` (`scripts/check-format.mjs`) |
| Docs links, ADR pages and package exports pages are valid | [Writing documentation](/docs/development/docs) | `[test]` `apps/web/src/lib/docs/source.test.ts` |
| Runtime and bundle changes are measured | [Conventions](/docs/development/conventions) | `[convention]` |
| Pagination strategy per endpoint | [Backend conventions](/docs/development/backend-conventions) | `[convention]` |

## Notes on the lint configuration [#notes-on-the-lint-configuration]

- **An oxlint override replaces a rule's options; it does not merge them.** Every `no-restricted-imports` override in `.oxlintrc.json` therefore repeats the system-pool paths of the base rule, and the most specific files come last. A ban added in one override disappears for files a later override matches unless it is repeated there. For the same reason, the mobile app's direction rules are a test (`scripts/mobile-feature-boundaries.test.mjs`): as lint rules they would have to repeat the whole client boundary inside `apps/mobile/src/.oxlintrc.json`.
- **D2 and D8 list the migrated features by path.** A feature that moves to tagged errors and an Effect service adds its `*.service.ts`, `*.rules.ts`, `*.policy.ts` and `*.procedures.ts` to those overrides in the same change. The comment above D2 names the features still to migrate; that list only shrinks.
- **Import cycles are a test, not a lint rule.** `import/no-cycle` needs oxlint's `import` plugin, whose module graph made `pnpm lint` about 3.7 times slower. `scripts/import-cycles.test.mjs` runs the rule on the two server trees inside `pnpm test`.
- **Nothing in the root overrides matches `apps/mobile`**, whose lint output stays unchanged.

## Next rules to automate [#next-rules-to-automate]

- **No network I/O inside `withTenant`.** A test could wrap the tenant handle and fail when a `fetch` starts inside the callback.
- **One `run*` per entry point.** A lint pattern could refuse `Effect.run*` outside `platform/rpc/procedure-edge.ts`, `platform/effect/edge.ts` and the job runner.
- **Tenant isolation on a deploy.** `rls-preview-proof.mjs` could run in the preview workflow after the runtime check.
- **Optional removal.** The six removal runs could become a scheduled CI job. Today the base run and `--remove-server-billing` pass; the MCP and task combinations stop at `packages/api` type errors that wait on a product decision about those recipes.

