Status: Proposed (2026-06-28) — kernel architecture assessment; execution gated to the cross-repo window (with ADR-0076 Step 2)
Deciders: ObjectStack Protocol Architects
Builds on: ADR-0076 (objectql core tiering — protocol relocated to its own package; lean ./core entry; Step 2 deferred), ADR-0066 (governance is pluggable, not in the engine), ADR-0025 / ADR-0033 (open-core boundary)
Consumers: @objectstack/spec (api/protocol.zod.ts — the contract), @objectstack/metadata-protocol (the impl), @objectstack/rest (dispatcher), @objectstack/objectql (engine + boot), and the domain services that already implement slices (service-analytics, service-realtime, service-messaging, …).
Premise: A deep review of the data kernel (prompted by "why does objectql depend on protocol, and protocol isn't only metadata") found that the engine is well-designed but the central wire contract is a god-interface. This ADR records that finding and the decision to segment the contract, separately from the packaging work already done in ADR-0076.
- The engine is the crown jewel and it is correct — protect it.
engine.ts(~3.3k LOC) hard-codes zero governance: RLS, RBAC, field-masking, owner stamping, tenantorganization_idare all in plugins viaregisterMiddleware+ hooks. The driver port is minimal (CRUD+DDL). Boot ordering is sound. Do not add protocol/governance concerns to the engine. - The real debt is
ObjectStackProtocol: a 70-method, 11-domain god-interface. Data(9) + Metadata(8) + Feed(13) + Notifications(7) + Realtime(6) + Packages(6) + Views(5) + Permissions(3) + Workflows(3) + AI(3) + i18n(3) + Analytics(2) + Automation(1). 60/70 are optional; no consumer uses >11% (REST ~11%, objectql ~2%, analytics ~3%). - Decision: segment the contract per ISP into
DataProtocol,MetadataProtocol, and optional capability protocols (Analytics/Feed/Realtime/Notification/View/…). Keep a composed aliasObjectStackProtocol = DataProtocol & MetadataProtocol & Partial<…>for back-compat. This is mostly spec/type-level and incremental. - The implementation and package naming follow the contract. Once split, the
DataProtocolimpl is thin (near-engine), theMetadataProtocolimpl is the heavy control plane; the capability domains are served by their already-existing services, not bundled.@objectstack/metadata-protocolis then accurately named (or split). Done at the cross-repo window with ADR-0076 Step 2 (cloud bootsObjectQLPlugindirectly in ~8 sites).
| Layer | Finding | Verdict |
|---|---|---|
IDataDriver port |
CRUD + DDL only | minimal, clean |
engine (objectql) |
in-process data primitive; zero governance hard-coded (verified: no RLS/permission/tenant logic in engine.ts); governance is 100% middleware/hooks (plugin-security/plugin-sharing/org-scoping) |
clean primitive — protect |
| capability seam | registerMiddleware((ctx,next)=>…) onion + per-object/priority hooks |
clean, well-defined |
ObjectStackProtocol |
70 methods, 11 unrelated domains, 60 optional, no consumer >11% | god-interface — the debt |
ObjectStackProtocolImplementation |
~5.6k LOC; large but maintainable (uses only public engine API; repository pattern isolates overlay) | size is driven by the god-interface |
The contract conflates the data plane (CRUD), the metadata control plane (sys_metadata mgmt), and a long tail of capability domains (feed, realtime, notifications, analytics, AI, i18n, …) that already have their own services. The interface merely aggregates them. Consequences:
- Anything that "implements the protocol" is nominally on the hook for 11 domains → forces a monolithic ~5.6k-LOC facade.
- The contract cannot be versioned — a change in any one domain perturbs the whole protocol.
- It is the shared root cause of the symptoms surfaced earlier: the misleading
metadata-protocolname (the package is not only metadata — it also carries the thin data facade), and the difficulty explaining "what protocol is."
ADR-0076 relocated the protocol implementation into its own package and added the lean @objectstack/objectql/core entry (shipped in #2415). That fixed packaging and the engine-embed story. It did not address the shape of the contract — that is this ADR.
No protocol, metadata-management, or governance logic enters engine.ts / @objectstack/objectql/core. Governance remains pluggable (ADR-0066). The ./core boundary ratchet (ADR-0076) guards this. This is the kernel's most valuable property; protect it over any tidiness goal.
Split the single interface in spec/api/protocol.zod.ts into focused contracts:
DataProtocol—findData/getData/createData/updateData/deleteData(+ batch). Thin wire-normalizers over the engine.MetadataProtocol—getMetaItem(s)/saveMetaItem/publishMetaItem/deleteMetaItem/getMetaTypes/getDiscovery/getUiView+ draft/publish, locks (ADR-0010), commits (ADR-0067), package ownership (ADR-0048),loadMetaFromDb. The heavy control plane.- Optional capability protocols —
AnalyticsProtocol,FeedProtocol,RealtimeProtocol,NotificationProtocol,ViewProtocol, … each owned by its existing service, each independently optional and versionable. - Back-compat: keep
ObjectStackProtocol = DataProtocol & MetadataProtocol & Partial<AnalyticsProtocol & FeedProtocol & …>as a composed alias so current callers/types keep working. The split is largely type-level and can land incrementally (define sub-interfaces first; narrow consumers over time).
DataProtocolimpl is thin (normalize → engine); it may live next to the engine-adjacent transport layer rather than inside the metadata package.MetadataProtocolimpl is the substantial control plane and is the true content of today's@objectstack/metadata-protocol.- Capability domains are served by their existing services (
service-analytics/service-realtime/service-messaging/ …), not re-bundled into one facade. - Rename: once the data facade is separated,
@objectstack/metadata-protocolis accurately the metadata-protocol package (or is renamed to reflect the data/metadata split). Treat the rename as breaking and bundle it with the cross-repo window.
- The contract segmentation (D2) is spec/type-level, mostly non-breaking via the composed alias, and may begin incrementally at any time.
- The impl/package restructure + rename (D3) and ADR-0076 Step 2 (making the engine package itself protocol-free) share the same blast radius — cloud constructs
ObjectQLPlugindirectly in ~8 sites — and must land in a coordinated framework+cloud window (a major). Do them together.
- The engine, the driver port, and the middleware/hook capability seam (ADR-0066/0076).
- Existing domain services (analytics/realtime/messaging) keep their implementations; only the contract stops aggregating them.
@objectstack/objectql/coreremains the lean embed entry.
- + A versionable, segregated contract; implementers depend only on the slice they serve; SDK/codegen per-domain; the ~5.6k-LOC facade is no longer forced.
- + Resolves the naming confusion at the root (data vs metadata vs capability domains become distinct).
- + The high-leverage part (D2) is incremental and low-risk (type-level + composed alias).
- − A full split touches
spec(the contract) and every implementer/consumer; the rename is breaking → must ride the coordinated cross-repo major. - Risk: doing D2 half-way (sub-interfaces defined but consumers never narrowed) yields churn with little gain. Mitigation: pair each new sub-interface with at least one consumer narrowed to it, and track coverage.
- Data facade home — does
DataProtocolimpl live in the engine-adjacent transport package, inrest, or in a small@objectstack/protocol-data? (It is thin and transport-shaped.) - Metadata package name — keep
@objectstack/metadata-protocolfor theMetadataProtocolimpl, or rename to@objectstack/metadata-runtime/@objectstack/protocol-metadata? - Per-domain versioning — once segmented, do capability protocols get independent version markers / a capability-discovery method (
getCapabilities())? - Order vs ADR-0076 Step 2 — segment the contract first (incremental) and let Step 2 + rename follow, or do all in one coordinated PR set? (Leaning: contract first, packaging at the window.)