Skip to content

Commit f115182

Browse files
os-zhuangclaude
andauthored
feat(spec): App as the consumer-facing unit (ADR-0019) (#1406)
* docs(adr): ADR-0019 App as the consumer-facing unit (Package ≡ App) Establish the App as the single consumer-facing noun: download = open = uninstall, one identity throughout. Internal composition (plugins, modules, drivers) becomes invisible bundle detail, like frameworks inside a .app. Remove the suite-contains-apps container; HotCRM folds into one App. Builds on ADR-0003. * docs(adr): ADR-0019 v2 — merge metadata/code plane split & capability contract Merge Axis B into ADR-0019: packages are two categories (metadata, hot- loaded; code, compiled into the server build), mapped to two runtime planes joined by a capability-by-reference contract. Add D5-D7 (two planes; consumer App must be pure metadata; requires/provides contract). Relax D4: an App owns a SET of namespaces (uninstall removes the set), so suites fold without object renames. Record honestly that the capability contract is mostly unbuilt today (closed CAPABILITY_PROVIDERS table + inert provides/requires schema) and surface the risks as Consequences and Open questions. * feat(spec): ADR-0019 P2 — manifest two-tier + consumer-App rules (warn-only) Add CONSUMER_INSTALLABLE_TYPES + isConsumerInstallable (D2 tier split), and a consumer-app-rules module with validateConsumerAppPurity (D6) and validateRequiresShape (D7). Wire both into defineStack as non-blocking warnings so existing code-bearing apps still build. Add tests + a P2 landing-plan doc. No runtime/registry changes (capability registry + install gate remain P4). * refactor(spec): narrow ADR-0019 to 'App as consumer unit'; revert plane/purity scope Per design review, narrow the scheme to its one cheap, certain part — the consumer surface is one App — and revert the metadata/code plane, D6 purity, and D7 capability-contract apparatus (premises did not hold: consumer apps legitimately carry code-bearing metadata like L2 sandboxed-JS hooks, and auto-review was dropped as a requirement). Lands: - isConsumerInstallable(type) + CONSUMER_INSTALLABLE_TYPES (single source of truth) - MarketplaceListingSchema.packageType constrained to consumer-installable types, so a non-App consumer listing cannot be represented (guarantee in the contract) Reverts: consumer-app-rules.{ts,test.ts}, the defineStack warn-only wiring, the P2 plan doc. ADR-0019 rewritten to v3 (decision = D1-D4; the larger scheme moved to an explicit, gated 'Out of scope' section). * feat(spec): enforce one App per package in defineStack (ADR-0019 D1/D3) A package with manifest.type === 'app' must define exactly one app. Zero is rejected (an app package with no app surface); more than one is the banned 'suite contains apps' shape — fold into one app with multiple tabs, or split into separate packages. Non-app package types are unconstrained. Mirrors the existing validateNamespacePrefix pattern. Existing examples already define exactly one app, so this is non-breaking. * fix(spec): relax one-App rule to 'at most one'; restore green suite The previous commit hard-required exactly one app for type:app packages, which broke 31 existing spec fixtures that use type:app with zero apps to test unrelated behavior. The banned case per ADR-0019 D3 is *multiple* apps (the suite shape), not zero. Relax validateSingleApp to reject >1 only; allow 0. Add tests. Full spec suite green (6628), tsc clean, compose-stacks + app-crm smoke green. * test(spec): cover at-most-one-App rule in defineStack (ADR-0019) Adds the validateSingleApp tests that failed to land in the prior commit: one app ok, zero apps ok, >1 app throws, non-app types unconstrained. * fix(spec): make at-most-one-App tests use cross-ref-valid apps The reject-case test used name+label-only apps, which trip the cross- reference landing check (runs before the single-app rule) and threw the wrong error. Give each app navigation to a defined object so the suite fixture reaches validateSingleApp. Full spec suite green (6628), tsc clean. * fix(spec): use valid 2-char app names in non-app-type test fixture Final fix for the at-most-one-App test suite: the driver fixture used single-char app names ('a'/'b') which fail the SnakeCaseIdentifier min-2-char rule and threw the wrong error. Use 'app_a'/'app_b'. Full spec suite green (6624 passed, 0 failed), tsc clean. * docs: align package-registry + marketplace-publishing to ADR-0019 Update the two docs that still described the old 'suite contains apps' model: - package-registry.zod.ts doc comment: App is the one consumer unit; a consumer package defines at most one app; internal contributions are invisible bundle parts. - marketplace-publishing.md §1-2 + §6: taxonomy rewritten around the App; HotCRM reference reframed as one App with internal plugins; §6 replaced with the two valid shapes (one App bundling plugins, or independent Apps) and an explicit 'no suite' note. * docs: finish ADR-0019 alignment in marketplace-publishing + package-registry Complete the partial doc update: package-registry.zod.ts header comment and marketplace-publishing.md sections 2.1/2.2/6 + summary now describe the App as the one consumer unit (at most one app per package; internal contributions invisible; no suite-contains-apps). Removes the remaining old 'suite' wording. * docs(spec): fix package-registry header to ADR-0019 (App is the consumer unit) The earlier 'docs: align ...' commit message claimed this file was updated, but the edit had silently failed — the header still described the old 'package may contain ... multiple apps (suite of applications)' model. Replace it with the ADR-0019 model: App is the one consumer-facing unit, at most one app per consumer package, internal contributions invisible. No 'suite'. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 25fedfd commit f115182

10 files changed

Lines changed: 425 additions & 68 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
ADR-0019 — App as the consumer-facing unit. The consumer Marketplace surfaces
6+
exactly one user-visible noun, the App.
7+
8+
- Adds `CONSUMER_INSTALLABLE_TYPES` and `isConsumerInstallable(type)` (the single
9+
source of truth for "what a consumer can install").
10+
- Constrains `MarketplaceListingSchema.packageType` to `CONSUMER_INSTALLABLE_TYPES`
11+
(default `app`) so a non-App (driver/server/plugin/…) listing cannot be
12+
represented — the "consumers see only Apps" guarantee is enforced in the data
13+
contract, not a forgettable query filter.
14+
- `defineStack()` now enforces **at most one App per package**: a package with
15+
`manifest.type === 'app'` may not define more than one app — the banned "suite
16+
contains apps" shape throws with a clear fix (fold into one app with multiple
17+
tabs, or split into separate packages). Zero apps is allowed; non-`app`
18+
package types are unconstrained. Non-breaking for existing stacks.
19+
20+
The package `type` enum is unchanged; the additions are non-breaking. No
21+
runtime/registry/execution changes.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# ADR-0019: App as the Consumer-Facing Unit
2+
3+
**Status**: Proposed (v3 — narrowed to the consumer-surface decision)
4+
**Date**: 2026-05-31
5+
**Deciders**: ObjectStack Protocol Architects
6+
**Builds on**: ADR-0003 (Package as First-Class Citizen), ADR-0006 (Three-Layer Tenancy), ADR-0016 (Studio Package Authoring & Publish)
7+
**Consumers**: `@objectstack/spec/kernel` (`plugin.zod.ts`), `@objectstack/spec/cloud` (`marketplace.zod.ts`), the Console `marketplace` UI, the Studio publish flow, `docs/design/marketplace-publishing.md`
8+
9+
> **Revision history**
10+
> - **v1/v2** explored a much larger scheme: a metadata/code *plane* split,
11+
> a "consumer App must be pure metadata" mandate (D6), a capability-by-
12+
> reference contract (D7), and auto-reviewed Marketplace trust lanes.
13+
> - **v3 deliberately narrows this ADR to its one cheap, certain, high-value
14+
> part: the consumer surface.** The larger scheme was dropped from scope
15+
> because the design review found its premises did not hold (consumer apps
16+
> legitimately contain code — e.g. L2 sandboxed-JS hooks — so "pure metadata"
17+
> is not real; and auto-review was dropped as a requirement). The remaining
18+
> hard problems (capability registry/versioning, sandbox hardening, shared
19+
> data model, data migration) are recorded in §Out of scope as a *separate,
20+
> gated* bet, not part of this decision.
21+
22+
---
23+
24+
## Context
25+
26+
ADR-0003 made the **package** a first-class versioned artifact, but said
27+
nothing about the **consumer mental model** — what a non-developer sees,
28+
installs, opens, and removes.
29+
30+
Today that surface is muddy. `manifest.zod.ts` exposes a **Package** that can be
31+
any of ten `type`s and may contain *0, 1, or N apps* (a "suite").
32+
`package-registry.zod.ts` codifies it:
33+
34+
> **Package**: the unit of installation. **App**: a UI navigation shell *inside*
35+
> a package. A package may contain zero apps (driver), one app (typical), or
36+
> multiple apps (suite).
37+
38+
The reference implementation **HotCRM** is the worst case: one `type: app`
39+
package aggregating 13 sub-plugins, each with its own namespace. A consumer
40+
cannot tell what they installed, what "opening" means, or what uninstall
41+
removes. This is the Microsoft-Office "suite contains applications" model — and
42+
its inherent confusion.
43+
44+
The Apple model is the opposite and the target: **there is no user-visible
45+
container above the app**. The thing you download *is* the one you open *is* the
46+
one you remove — one noun. Even iWork ships as three separate apps, not a suite.
47+
48+
---
49+
50+
## Decision
51+
52+
### D1 — The App is the only consumer-facing unit
53+
54+
The thing a user downloads, opens, and uninstalls is exactly one **App**. The
55+
word "package" is retained as an internal / developer / control-plane term (the
56+
ADR-0003 artifact) but is **never surfaced to consumers**. A Marketplace
57+
**listing is an App listing**.
58+
59+
### D2 — Only `type: app` is consumer-installable; everything else is invisible
60+
61+
The package `type` enum is unchanged, but a semantic split is layered over it:
62+
63+
| Tier | Types | Consumer-installable? | In consumer Marketplace? |
64+
|---|---|---|---|
65+
| **Consumer unit** | `app` | Yes | Yes (App listing) |
66+
| **Internal contribution** | `plugin`, `driver`, `server`, `ui`, `theme`, `agent`, `objectql`, `module`, `adapter` | No | No |
67+
68+
Internal contributions are the "frameworks inside the `.app` bundle": they ship
69+
inside an App or are operator-provisioned, and are never independently browsed
70+
or installed by a consumer. The consumer Marketplace filters to `type: app` via
71+
`isConsumerInstallable(type)` (`packages/spec/src/kernel/plugin.zod.ts`).
72+
73+
### D3 — Suites fold into a single App (no aggregator above App)
74+
75+
The "package contains N apps (suite)" shape is removed from the consumer model.
76+
A vertical solution that today aggregates many plugins MUST resolve to **one
77+
App** whose internal plugins are invisible. A developer who genuinely wants
78+
independent products ships them as **independent Apps** (the iWork route), never
79+
as a wrapper that surfaces N apps.
80+
81+
### D4 — An App owns a set of namespaces; uninstall removes the set
82+
83+
An App declares and owns a set of namespaces; uninstall is atomic over that set.
84+
(The runtime already supports this: `objectql/src/registry.ts` keys namespaces
85+
as `Map<namespace, Set<packageId>>`.) Folding a suite like HotCRM therefore
86+
needs no object rename — the single App simply owns the namespaces its former
87+
sub-plugins used.
88+
89+
---
90+
91+
## Scope of this ADR (what lands)
92+
93+
- **Decision**: D1–D4 above — the consumer surface is one App.
94+
- **Code (landed)**:
95+
- `CONSUMER_INSTALLABLE_TYPES` + `isConsumerInstallable(type)` in
96+
`plugin.zod.ts` — the single source of truth (additive, non-breaking).
97+
- `MarketplaceListingSchema.packageType` constrained to
98+
`CONSUMER_INSTALLABLE_TYPES` (`marketplace.zod.ts`) — the data contract now
99+
**cannot represent a non-App consumer listing**, so the "consumers see only
100+
Apps" guarantee is enforced at the schema level rather than left to a query
101+
filter that could be forgotten.
102+
- **Follow-up (thin, surfaces outside this package)**: have the Console
103+
`marketplace` UI / cloud catalog populate `packageType` from the manifest
104+
(the catalog is cloud-served and proxied verbatim by
105+
`runtime/cloud/marketplace-proxy-plugin.ts`), and fold HotCRM to a single App
106+
(D3/D4). Both consume the same `isConsumerInstallable` predicate.
107+
108+
Nothing in this ADR changes the runtime, the registry, or how packages execute.
109+
110+
---
111+
112+
## Out of scope (explored, deferred — a separate gated bet)
113+
114+
The following were explored in v1/v2 and **deliberately removed** from this
115+
decision. They form a distinct, multi-quarter platform investment that should
116+
get its own ADR and an explicit go/no-go — they are **not** prerequisites for
117+
D1–D4:
118+
119+
- **Metadata/code runtime planes** (hot-loadable sandboxed source vs npm code
120+
baked into the server image). Useful as a *descriptive* mental model; not a
121+
decision here.
122+
- **"Consumer App must be pure metadata" (old D6).** Dropped — apps legitimately
123+
carry code-bearing metadata such as L2 sandboxed-JS hooks
124+
(`data/hook.zod.ts`: `body.language: 'js'` with declared `capabilities`).
125+
"Pure metadata" is not a real boundary.
126+
- **Capability-by-reference contract + open capability registry + install gate
127+
(old D7).** The real long-term hard problem (versioning / semantic drift); the
128+
current mechanism is the closed `CAPABILITY_PROVIDERS` table in
129+
`runtime/src/cloud/capability-loader.ts` plus an inert `provides/requires`
130+
schema.
131+
- **Auto-reviewed Marketplace trust lanes.** Dropped as a requirement.
132+
- **The runtime safety boundary** for self-serve code-bearing apps: the **L2
133+
hook sandbox + capability/permission enforcer**
134+
(`core/src/security/plugin-permission-enforcer.ts`). If a self-serve,
135+
code-bearing Marketplace is ever pursued, *this* — not review, not a purity
136+
rule — is the load-bearing wall, and its hardness must be proven first.
137+
- **Shared/published data model + inter-App dependencies** (cross-app data
138+
sharing without a "suite").
139+
- **Metadata-with-data migration framework** (backfill, destructive-change
140+
confirmation, reversible uninstall of object data).
141+
142+
---
143+
144+
## Consequences
145+
146+
**Positive**
147+
- One noun for consumers: install = open = uninstall, the model everyone already
148+
knows from their phone. Cheap, low-risk, independently shippable.
149+
- The confusing "package / suite" abstraction disappears from the consumer view.
150+
- No runtime, registry, or execution changes — the blast radius of this ADR is
151+
the Marketplace/Console listing surface plus one additive predicate.
152+
153+
**Negative / costs**
154+
- HotCRM and any existing suite-style packages must fold to a single App (D3/D4).
155+
- "Publish a standalone consumer plugin" is no longer a consumer path; plugins
156+
ship inside an App or are operator-provisioned.
157+
158+
---
159+
160+
## Alternatives considered
161+
162+
1. **Keep the suite model, improve the install UX.** Rejected: a 1-to-N noun is
163+
a structural problem; wording cannot fix it.
164+
2. **Expose both "App" and "Suite" as consumer units.** Rejected: reintroduces
165+
the container-above-app this ADR removes.
166+
3. **The full v1/v2 scheme (planes + purity + capability contract + trust
167+
lanes).** Deferred — see §Out of scope. Its premises did not hold and its
168+
hard parts are a separate, gated investment.
169+
170+
---
171+
172+
## References
173+
174+
- ADR-0003 — Package as First-Class Citizen with Versioned Releases
175+
- ADR-0006 — Three-Layer Tenancy (Organization, Project, Environment)
176+
- ADR-0016 — Studio Package Authoring & Publish
177+
- `packages/spec/src/kernel/plugin.zod.ts` (`isConsumerInstallable`)
178+
- `packages/spec/src/cloud/marketplace.zod.ts`
179+
- `docs/design/marketplace-publishing.md`

docs/design/marketplace-publishing.md

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
> **Author:** ObjectStack Core Team
44
> **Created:** 2026-02-17
5+
> **Updated:** 2026-05-31 — §1–2 and §6 aligned to ADR-0019 (App as the
6+
> consumer-facing unit; no "suite contains apps"; consumer Marketplace lists
7+
> only `type: app`).
58
> **Status:** Design Specification
69
> **Target Version:** v3.2 – v4.0
710
@@ -35,33 +38,44 @@ The marketplace protocol consists of three primary schema files:
3538

3639
### Reference Implementation
3740

38-
**HotCRM** (`objectstack-ai/hotcrm`) demonstrates a real-world metadata-only application with:
39-
- Root `objectstack.config.ts` with `type: 'app'` aggregating 13 plugins
40-
- 13 sub-plugins (crm, finance, marketing, products, support, hr, analytics, integration, community, healthcare, real-estate, education, financial-services)
41-
- 30+ metadata types per plugin
41+
**HotCRM** (`objectstack-ai/hotcrm`) demonstrates a real-world application. Per
42+
ADR-0019, it is being refolded into a **single consumer App** (download = open =
43+
uninstall) whose internal plugins are invisible "frameworks inside the bundle":
44+
- Root `objectstack.config.ts` with `type: 'app'` — one consumer-facing App
45+
- The former sub-plugins (crm, finance, marketing, …) are internal
46+
contributions bundled inside the App, not separately listed or installed
47+
- The App owns the set of namespaces those contributions use; uninstall is
48+
atomic over that set
4249

4350
---
4451

4552
## 2. Architecture Overview
4653

47-
### 2.1 Package Taxonomy
54+
### 2.1 Package Taxonomy (ADR-0019)
4855

49-
The ObjectStack ecosystem uses a clear hierarchy:
56+
The ObjectStack ecosystem exposes **one consumer-facing noun: the App.**
5057

5158
```
52-
Package (ManifestSchema)
53-
├── Type: app | plugin | driver | server | ui | theme | agent | module
54-
├── Namespace: Scoped identifier (e.g., "crm" → crm__account, crm__deal)
55-
└── May Contain: Zero apps | One app | Multiple apps
59+
App ← the only consumer unit (download = open = uninstall)
60+
├── Type: app (the only consumer-installable type; see isConsumerInstallable)
61+
├── Namespace(s): the App owns a set; uninstall is atomic over the set
62+
├── Apps: at most one app surface per package (one app, many tabs — no suite)
63+
└── Internal contributions (invisible): plugin | driver | server | ui | theme | agent | module
64+
— "frameworks inside the .app bundle": bundled or operator-provisioned,
65+
never independently listed or installed by a consumer
5666
```
5767

58-
### 2.2 Package vs App vs Plugin
68+
### 2.2 App vs Package vs internal contribution
5969

60-
From `packages/spec/src/kernel/package-registry.zod.ts`:
70+
From `packages/spec/src/kernel/package-registry.zod.ts` (ADR-0019):
6171

62-
> **Package**: The unit of installation — a deployable artifact containing metadata
63-
> **App**: A UI navigation shell defined inside a package
64-
> A package may contain zero apps (driver), one app (typical), or multiple apps (suite)
72+
> **App**: the one consumer-facing unit — what a tenant downloads, opens, and
73+
> uninstalls. Only `type: app` is consumer-installable, and a consumer package
74+
> defines **at most one app** (no "suite contains apps").
75+
> **Package**: the internal / control-plane artifact term; never surfaced to
76+
> consumers as a separate noun.
77+
> **Internal contributions** (plugin/driver/server/…): ship inside an App or are
78+
> operator-provisioned; a consumer never installs them directly.
6579
6680
### 2.3 Namespace Scoping
6781

@@ -353,56 +367,51 @@ Steps:
353367

354368
---
355369

356-
## 6. Publishing Strategies for Multi-Plugin Applications
370+
## 6. Composing a Multi-Plugin Application (ADR-0019)
357371

358-
### 6.1 HotCRM Architecture
372+
The consumer model has **one user-visible noun — the App — and no "suite
373+
contains apps" aggregator**. A solution built from many internal plugins has
374+
exactly two valid shapes; both ship as *one App* (or several independent Apps)
375+
to the consumer, never as a wrapper that surfaces N apps.
359376

360-
13 plugins: crm, finance, marketing, products, support, hr, analytics, integration, community, healthcare, real-estate, education, financial-services
377+
### 6.1 Shape A — One App, internal plugins bundled (recommended for HotCRM)
361378

362-
### 6.2 Strategy A: Suite Mode
379+
Compose all internal plugins into a single `type: 'app'` package. The plugins
380+
are "frameworks inside the .app bundle" — invisible to the consumer. The App
381+
defines **at most one app surface** (one app with multiple tabs) and owns the
382+
set of namespaces its plugins use.
363383

364-
**Approach:** Bundle all 13 plugins in one package
365-
366-
**Pros:** Simple install, guaranteed compatibility, single version
367-
**Cons:** Large size, no modularity
368-
**Use Case:** Enterprise bundle
369-
370-
### 6.3 Strategy B: Individual Listings
371-
372-
**Approach:** Each plugin published separately
373-
374-
**Pros:** Modular, pay-per-module, independent versioning
375-
**Cons:** Complex dependencies, version matrix
376-
**Use Case:** Marketplace à la carte
377-
378-
### 6.4 Strategy C: Hybrid (Recommended)
379-
380-
**Approach:** Core bundle + industry extensions
381-
382-
**Base Package:** crm + finance + products + support + hr + analytics + integration
384+
```typescript
385+
// objectstack.config.ts
386+
export default defineStack({
387+
manifest: { id: 'com.hotcrm', type: 'app', namespace: 'hotcrm', version: '2.0.0' },
388+
plugins: [crmPlugin, financePlugin, marketingPlugin, /**/], // invisible internals
389+
apps: [hotcrmApp], // exactly one consumer app surface (multiple tabs), not a suite
390+
});
391+
```
383392

384-
**Extensions (separate):**
385-
- healthcare
386-
- real-estate
387-
- education
388-
- financial-services
393+
**Pros:** one listing, one install, atomic versioning and uninstall, one noun
394+
for the consumer.
389395

390-
**Pros:** Balanced, core always available, optional verticals
391-
**Use Case:** Commercial SaaS with verticals
396+
### 6.2 Shape B — Several independent Apps (the iWork split)
392397

393-
### 6.5 Dependency Declaration
398+
If verticals are genuinely separate products, ship each as its **own App
399+
package** — its own listing, install, namespace, and lifecycle (like Pages /
400+
Numbers / Keynote). This is *not* a suite: there is no wrapper above them.
394401

395-
```typescript
396-
export default defineStack({
397-
id: 'com.hotcrm.healthcare',
398-
dependencies: {
399-
'com.hotcrm.base': '^1.0.0',
400-
'com.hotcrm.analytics': '^1.1.0'
401-
}
402-
});
402+
```bash
403+
os publish # com.hotcrm.crm (type: app)
404+
os publish # com.hotcrm.finance (type: app)
403405
```
404406

405-
Kernel resolves graph, installs deps first, validates versions, rejects cycles.
407+
**Pros:** granular adoption, independent versioning.
408+
**Cons:** verticals do not share one install; cross-app data sharing is a
409+
separate concern (see ADR-0019 §Out of scope).
410+
411+
> **Not allowed:** a "meta-package" / suite whose only job is to surface N apps
412+
> to the consumer — the Office "suite contains applications" model ADR-0019
413+
> removes. `defineStack` enforces *at most one app* per `type: app` package, and
414+
> the consumer Marketplace lists only `type: app`.
406415
407416
---
408417

@@ -591,7 +600,7 @@ This comprehensive design document establishes the **Marketplace Protocol** for
591600
2. **Publishing**: 6-step flow (develop → validate → build → submit → review → publish)
592601
3. **Installation**: 4 channels (CLI, SDK, API, UI) with 30+ metadata type registration
593602
4. **Lifecycle**: State machine for install/enable/disable/upgrade/uninstall
594-
5. **Strategies**: 3 approaches for multi-plugin apps (Suite, Individual, Hybrid)
603+
5. **Composition** (ADR-0019): one App (internal plugins bundled) or several independent Apps — never a suite
595604
6. **Security**: 5-level verification, signing, automated scanning, permission review
596605
7. **Pricing**: 6 models (free to enterprise) with license validation
597606
8. **Roadmap**: 5 phases across 4 quarters

0 commit comments

Comments
 (0)