Context
ADR 0001 gave every member a place. Nothing yet says which member may depend on which. Without a direction rule a library can import an app “just for a type”, and the library can then no longer be built or shipped without that app. The easy edge to add by accident is library to app, not library to service.Decision
Every workspace member carries exactly one tier tag in its ownturbo.jsonc ("tags": ["tier:<x>"]), and the root
turbo.jsonc boundaries block denies each tier everything above it.
- A library never imports an app, a service or
encore.dev. If a library needs a shape from above, the shape moves down into a contract package (0117). - An app reaches a service over the wire only, through a generated client, never through its source.
- A service never imports another service’s internals (0010).
- The tier is implied by the directory. A member whose tag disagrees with its directory fails.
Consequences
- A package stays usable by any number of deployables.
- Moving a member between tiers is an ADR, not a rename.
- Product literals and
process.envin packages are a related rule; see/constitution/boundaries.
Enforcement
turbo boundaries(pnpm boundaries, part ofpnpm verify) reads the tags.tools/scripts/validate-repository.mjsrequires exactly one tier tag, equal to the one the directory implies.packages/tooling/oxlint/src/boundaries.jsoncbans imports ofapps/**,services/**,encore.devand~encore/*frompackages/**.- Planned: a check that packages contain no product literal and read no
process.env.
Supersedes and sources
Supersedes: none. Sources: basaltadr/0021-tier-dependency-direction; figentra docs/constitution/boundaries.md.