ADR 0003: Program as the tenant scope and the 72-permission catalog
- Status
- Accepted
- Date
- Deciders
- Founder, product team
Context
The reference profile scopes every record to an organisation and a generic “space”. Trovensa’s brief describes teams working inside one development program at a time, with milestones, dependencies and approvals per program. Calling the scope “space” would put two words on one idea, and calling the fourth product “Programs” while the scope is a program would collide. The scaffolded gate web/scripts/check-permissions-sync.mjs parsed _org( / _space( helpers and the export SPACE_PERMISSIONS (lines 27 and 35 before the patch); api/tests/route_matrix.py carried SPACE_PREFIX = "/spaces/{space_id}".
Decision
- The tenant scope is Organization -> Program. Scope kinds are
organdprogram; program routes live under/api/v1/programs/{program_id}/...; the web tree is/p/[slug]/...; hooks areuseProgram().can(). - The fourth product is named Progress (ADR 0002).
- The permission catalog is pinned at 72 strings: org 13, program 13, evidence 13, plans 14, review 10, progress 9 (PRODUCT-CONTRACT.md section 3 lists them in order). Verbs come from the closed set
read create update delete manage approve link sync use read_own manage_owners. - Ten personas:
org_owner,org_admin,org_member,org_auditor,program_owner,program_lead,scientist,quality_reviewer,manufacturing_specialist,viewer, expanded from patterns at import time and seeded by migration0001. - The workspace copy of the permissions sync gate is patched to
_(org|program)(andPROGRAM_PERMISSIONS;route_matrix.pyusesPROGRAM_PREFIX. The plugin templates are not changed.
Consequences
- Every scoped table carries
organization_id,program_idandstage_id; every scoped query filters by them. - The web mirror, the docs permissions page and the API catalog must agree on 72 keys in the same order;
check-permissions-syncfails otherwise. - Adding a permission is a contract change: bump the count in the API, the web mirror and the docs page in one commit.
- Approval routes are separate from transition routes so each route has exactly one permission (
.managefor draft/review/archived transitions,.approvefor approvals).
Alternatives considered
- Keep “space” and put program records inside a space: gate-compatible without a patch, but two words for one idea in the UI; rejected.
- Scope = program and keep product “Programs”: acceptable collision but confusing navigation (“Programs” inside a program); rejected.
- Fewer permissions (one
manageper product): simpler catalog but no way to separate approvers from authors, which the brief requires; rejected.
Follow-ups
- Add
scripts/check-permissions-sync.test.mjsin the web repo covering the patched parser (web writer). - Record the measured route count (117 designed) in the claims register on publish day.