Layout
Modules
Every new feature goes insrc/modules/{module}/:
index.ts is a curated contract, not a barrel export. Only what other modules
legitimately need is exported; everything else is internal. Cross-module code imports
from the contract, never from another module’s services/ directly.
The shared layer
src/lib/ holds what genuinely crosses modules:
Route conventions
Boundary rules
No database access outside services or src/lib/
No database access outside services or src/lib/
A route handler that calls
prisma directly has put business logic in the transport layer.
Route handlers orchestrate; services decide.Server-only code stays server-only
Server-only code stays server-only
Modules with AI, XLSX or e-mail services keep them behind the contract and never import them
into client components. The observability logger in
lib/ is intentionally client-safe and does
not import the App Insights sink; the module-level logger does, and is server-only.No environment access in business logic
No environment access in business logic
Read
process.env at a module boundary and pass values in as parameters. A service that reads
its own configuration cannot be tested without the environment.Zod parses at the boundary
Zod parses at the boundary
Parse external input — HTTP bodies, database JSON, third-party API responses — at the entry
point, then pass typed values inward.
Size limits
Component files are limited to 200 lines by convention, warned past 200 by ESLint, and failed past 800 unless the file is grandfathered in a baseline. Grandfathered files may shrink but never grow — the ratchet only turns one way.Events
Modules communicate through the event bus rather than by importing each other:{module}.{entity}.{action}. Handler registration happens through
side-effect imports in the instrumentation entry point, not through the module
contract — see the pre-boarding orchestrator for the reference implementation.