# 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.
