# 0002 — Nx monorepo with enforced module boundaries - **Status:** Accepted - **Date:** 2026-05-06 ## Context and problem statement Fog Expedition has at least three distinct deployable surfaces (Twitch Video Overlay frontend, NestJS EBS backend, content/balance library), and types must be shared between them (DTOs, domain models, encounter records). The project is being built with heavy AI assistance, which adds a specific failure mode: AI assistants happily reach across project boundaries with relative imports if not constrained. How should the codebase be structured? ## Decision drivers - **Type sharing without duplication.** The overlay and EBS exchange `Mission`, `Survivor`, `EncounterResult` etc. Hand-keeping types in sync across separate repos is bug-prone. - **AI-assisted-build discipline.** AI tools generate plausible-looking code that violates architectural conventions unless those conventions are mechanically enforced. - **Solo-developer simplicity.** Multiple repos mean multiple CI configs, multiple versioning streams, multiple onboarding paths. - **Future scaling.** Should the project ever grow to more contributors, the architecture should still be coherent. - **Tooling support for the chosen language stack** (TypeScript, Angular, NestJS). ## Considered options 1. **Multi-repo (polyrepo).** Frontend, backend, and shared types each in their own repository. Shared types published to a private npm registry. 2. **Loose monorepo (npm/pnpm workspaces only).** All projects in one repo, no orchestrator. No automated boundary enforcement. 3. **Nx monorepo with `enforce-module-boundaries`.** All projects in one repo. Project tags (`scope:api`, `scope:overlay`, `scope:shared`, `type:app`, `type:lib`) define dependency rules enforced at lint time. 4. **Turborepo.** Lighter monorepo orchestrator, focused on caching and task running. No native module boundary enforcement. ## Decision outcome **Chosen: Nx monorepo with enforced module boundaries.** Project structure: ``` apps/ api/ # scope:api, type:app overlay/ # scope:overlay, type:app libs/ api-interfaces/ # scope:shared, type:lib (Zod schemas, DTOs) mission-logic/ # scope:shared, type:lib (encounter resolver, perk math) encounter-library/ # scope:shared, type:lib (content + helpers) ``` Boundary rules enforced via `@nx/enforce-module-boundaries`: - Apps can only depend on libs (not other apps). - `scope:api` can depend on `scope:api` and `scope:shared`. - `scope:overlay` can depend on `scope:overlay` and `scope:shared`. - `scope:shared` can only depend on `scope:shared`. Violations fail lint (verified at workspace setup with a deliberate negative test). ## Consequences ### Positive - Type sharing is direct via TypeScript imports, no version coordination needed. - Module boundary enforcement catches architecturally incorrect AI-generated code at lint time before code review. - Single CI pipeline, single dependency tree, single onboarding flow. - `nx affected` runs only what changed, keeping CI fast as the project grows. - Per-project `package.json` files keep workspace libs publish-ready (e.g., `api-interfaces` could later be published as a community SDK without restructuring). ### Negative - Steeper learning curve than plain pnpm workspaces — Nx generators, executors, and project configuration are non-trivial. - Tighter coupling to Nx's release cadence; `nx migrate` is required for major version upgrades but generally handles them well. - Some Nx generators lag behind the underlying tooling (e.g., `@nx/nest` doesn't yet support Vitest natively — see ADR-0003). ### Neutral - The monorepo single source of truth makes architectural mistakes more visible; it also makes them more impactful when they happen.