0007 — TypeScript strict mode everywhere, no exceptions
- Status: accepted
- Date: 2026-05-06
- Deciders: Derek
Context
For a multi-tenant system, the cost of “I forgot to scope this by org_id” is data leakage between organizations. The cost of an any somewhere is exactly that — the type system can’t catch the bug, and the test suite has to be perfect to compensate.
Combined with TDD posture (ADR 0009), strict TypeScript is the second leg of the stability stool.
Decision
Every package and app uses tsconfig.base.json which sets strict: true, noUncheckedIndexedAccess: true, noImplicitOverride: true, noFallthroughCasesInSwitch: true, exactOptionalPropertyTypes: true. No any without a // @ts-expect-error: <reason> or a written justification in a comment. No // @ts-nocheck ever.
Zod schemas at every external boundary (HTTP requests, DB rows, env vars, config files). The Zod schema is the source of truth; TS types are derived via z.infer<>.
Consequences
Easier:
- Most “I forgot to scope by org” bugs become compile errors
- Refactors are safe (rename across the workspace, type system finds the misses)
- AI agents working in unfamiliar parts of the codebase get instant feedback on bad assumptions
- The schema is the documentation
Harder:
- Porting internalize JS to TS is line-by-line work; we don’t shortcut it
- Generic types in the DB layer (Supabase clients with org-scoping) require careful design
Trip-wires
If a single any slips in, the reviewer (human or AI) flags it and writes the explicit comment or replaces with a real type. We don’t tolerate accumulating type debt — at this size, every escape valve is a foothold for future bugs.
Alternatives considered
- Gradual TS adoption (some packages JS, some TS). Boundaries between the two get messy fast; type information stops at the JS boundary, propagating loose types into TS. Rejected.
- Turn off
noUncheckedIndexedAccess. It’s annoying. It’s also the strictness setting that catches the most real-world bugs in array/record access. Worth the friction.