Found by building a real app against the documented surface (objectstack-ai/ats). A three-audience recruiting app was designed, carded and half-implemented as three apps — platform, employer, seeker — before defineStack threw:
An 'app' package must define at most one app, but found 2 (ats_admin_app, ats_employer_app) ... (ADR-0019 D3)
The rule is right and well-argued. The problem is that no surface an app author actually reads states it.
Measured gaps
| Surface |
mentions the one-app rule |
skills/objectstack-ui/SKILL.md — the skill for authoring *.app.ts |
0 |
content/docs/ui/audience-based-interfaces.mdx — the page on serving multiple audiences |
0 |
packages/spec/src/stack.zod.ts:1418 |
the throw itself |
docs/adr/0019-app-as-consumer-unit.md D3 |
the decision |
The constraint is enforced at the very last moment and documented only in the ADR — a file an app author has no particular reason to open.
audience-based-interfaces.mdx is the sharpest miss. It is the page about giving different audiences different surfaces, and its worked example is a single crm app gated by requiredPermissions — the correct pattern. But its scenario is framed as builder-vs-end-user, and it never says "and one app is all you may have". A reader designing for three business audiences sees a page about gating, not a page about a cap, and reasonably reaches for one app per audience.
The citation is ambiguous too
ADR-0019 names two different records in this repo:
docs/adr/0019-app-as-consumer-unit.md — D3 is the one-app rule
docs/adr/0019-approval-as-flow-node.md — D3 is deprecating ApprovalProcessSchema
Every ADR-0019 mention in the published skills bundle (objectstack-automation, objectstack-platform) refers to the approvals record. So an author who follows the throw's own citation is as likely to land on the wrong ADR as the right one — and both have a D3. This is the same defect class as #11781 (a bare ADR-0057 shared by two records).
What would have prevented it
Any one of these, in rough order of value:
- Say it in
audience-based-interfaces.mdx — one sentence in Recommended solution: a package of type: 'app' exposes exactly one App; multiple audiences are navigation groups inside it, gated by requiredPermissions. The page already demonstrates this; it just never claims it.
- Say it in the
objectstack-ui skill, in the App Navigation section, where an agent authoring *.app.ts will actually read it.
- Disambiguate the citation — the throw and the skills should cite
ADR-0019 (app-as-consumer-unit) D3, not a bare number two records answer to.
- Optionally, catch it earlier than
defineStack — a lint rule would name it at author time rather than at load.
Why this is worth more than a docs nit
The failure is cheap to hit and expensive to have designed around. The throw is loud and immediate, so nobody ships a broken app — but by the time it fires, the architecture document, the work cards and part of the implementation have all been written against a shape the platform refuses. In this case that was a design section, a backlog card and one dispatched implementation, all of which had to be re-ruled and re-done.
The agent that hit it stopped and asked rather than forcing it, which is the good outcome. It should not have needed to.
Found by building a real app against the documented surface (
objectstack-ai/ats). A three-audience recruiting app was designed, carded and half-implemented as three apps — platform, employer, seeker — beforedefineStackthrew:The rule is right and well-argued. The problem is that no surface an app author actually reads states it.
Measured gaps
skills/objectstack-ui/SKILL.md— the skill for authoring*.app.tscontent/docs/ui/audience-based-interfaces.mdx— the page on serving multiple audiencespackages/spec/src/stack.zod.ts:1418docs/adr/0019-app-as-consumer-unit.mdD3The constraint is enforced at the very last moment and documented only in the ADR — a file an app author has no particular reason to open.
audience-based-interfaces.mdxis the sharpest miss. It is the page about giving different audiences different surfaces, and its worked example is a singlecrmapp gated byrequiredPermissions— the correct pattern. But its scenario is framed as builder-vs-end-user, and it never says "and one app is all you may have". A reader designing for three business audiences sees a page about gating, not a page about a cap, and reasonably reaches for one app per audience.The citation is ambiguous too
ADR-0019names two different records in this repo:docs/adr/0019-app-as-consumer-unit.md— D3 is the one-app ruledocs/adr/0019-approval-as-flow-node.md— D3 is deprecatingApprovalProcessSchemaEvery
ADR-0019mention in the published skills bundle (objectstack-automation,objectstack-platform) refers to the approvals record. So an author who follows the throw's own citation is as likely to land on the wrong ADR as the right one — and both have a D3. This is the same defect class as #11781 (a bareADR-0057shared by two records).What would have prevented it
Any one of these, in rough order of value:
audience-based-interfaces.mdx— one sentence in Recommended solution: a package oftype: 'app'exposes exactly one App; multiple audiences are navigation groups inside it, gated byrequiredPermissions. The page already demonstrates this; it just never claims it.objectstack-uiskill, in the App Navigation section, where an agent authoring*.app.tswill actually read it.ADR-0019 (app-as-consumer-unit) D3, not a bare number two records answer to.defineStack— a lint rule would name it at author time rather than at load.Why this is worth more than a docs nit
The failure is cheap to hit and expensive to have designed around. The throw is loud and immediate, so nobody ships a broken app — but by the time it fires, the architecture document, the work cards and part of the implementation have all been written against a shape the platform refuses. In this case that was a design section, a backlog card and one dispatched implementation, all of which had to be re-ruled and re-done.
The agent that hit it stopped and asked rather than forcing it, which is the good outcome. It should not have needed to.