Tiers
Every workspace member carries exactly one tier tag in itsturbo.jsonc. The tag matches its directory.
The direction is one way. A package never imports an app, a service, or
encore.dev. An app never imports a service’s code. A package never imports another package’s src/: it uses the package’s declared exports. See ADR 0003 and the package standard.
Enforcement: enforced. Turborepo boundaries tags in turbo.jsonc, the oxlint no-restricted-imports rules in packages/tooling/oxlint/src/boundaries.jsonc, and the tier check in tools/scripts/validate-repository.mjs. All run in pnpm verify.
Placement test
Where does new code go?- Would a second, unrelated product still need this verbatim? Yes: it is a package in
packages/<category>/. - Does it only compose packages into something that runs or ships? A deployable: a service in
services/<name>or an app inapps/<name>. - Would it need renaming to make sense elsewhere? It belongs in the app or service that owns it. It is not a package.
Shared packages hold mechanisms, apps hold definitions
A package never contains a product enumeration or literal: no brand name, locale list, project id, URL, tenant id or environment value. It exposes decorators, base classes and typed config contracts; there is nodefine*() API (0094). The definitions (decorated classes and config objects) live in the app or service that owns them. A convenience value such as a default locale list is fine. A closed union type of product names is not.
A package never reads process.env. Configuration reaches it as an argument or a typed config object that the app builds.
Enforcement: partial. FORBIDDEN_DEPENDENCIES in the validator blocks forbidden packages. A check for product literals and process.env in packages/**/src is planned (a validator rule or an oxlint rule). Until then a literal in a shared package is a review finding.
Service ownership
Services are independent workspace members:services/<name> is @figentra/service-<name> with entry src/main.ts. Service source is plain TypeScript; Encore is the generated runtime target in .stackra/encore/ (0091, 0124). Until stackra gen exists, the rules below describe the target. They apply from the first service that owns data.
A service owns its API, its database, its schema and migrations, its events, its tests, a README.md and an AGENTS.md with a PII table. No other service reads that database or imports that service’s internals.
Enforcement: partial. The validator checks the service-unit files. Cross-service import and database-read rules are planned with the first service that has a database.
What a contract is
A shared-types folder is not a contract. A contract is a package’s types-only./contracts entry: a stable boundary with a version and a change policy, with no behaviour and no runtime dependency. Shared primitives (PublicId, Actor, EventEnvelope, Page, errors) live in @stackra/contracts. There are no separate <domain>-contracts packages, and cross-service types live in a contract and nowhere else (0117).