Tracking issue for ADR-0105 (proposed via #3499).
Status: Phase 0, Phase 1, and Phase 2 D8+D9 COMPLETE (re-evaluated 2026-07-30 against ADR-0108/0111/0112/0113 — see the re-evaluation comment). Engine landed in #3559 (activation corrected in #3570, ADR amended in #3603; driver union scope fixed in #3631); console affordances in objectstack-ai/objectui#2858; acceptance met by the three-plants-one-group dogfood (objectstack-ai/cloud#882 + objectstack-ai/cloud#883 — 9/9 assertions passing in BOTH entitlement states). D8 complete end-to-end (#3645 → #3663 → #3674 → #3695 → #3722 → #3767; the membership-role channel its placement replaces is closed by ADR-0108/#3802); D9 landed (#3824 + #3873). Remaining: D10 (blocked on its follow-up ADR), D13, "all my organizations" affordances, Phase 3. ADR status line updated in #4028.
Summary
Group-shaped customers (multi-plant manufacturing, multi-branch retail, holding structures) need one shared database, organization as the membership boundary, group-wide visibility, and cross-org workflows — a middle posture between today's single and multi. The ADR adds a group tenancy posture with MOAC-style union semantics (organization_id IN accessible_org_ids), engine-enforced, preserving ADR-0095 W1/W2 invariants.
Plan evaluation (2026-07-27)
Findings verified against main: F1 (security-plugin.ts substring strip — #3539), F2 (organization_admin wildcard VAMA + auto-org-admin-grant.ts — #3540), F3 (PermissionSet.contextVariables, ExecutionContext.rlsMembership, territory.zod.ts — all consumer-less), F4 (ADR-0092 whitelist has one entry). The technical premises held.
Design assessment: sound, and it survived implementation. D2 widened the existing Layer 0 predicate from equality to set membership; computeTenantLayer0Filter was already a pure function with the PLATFORM_ADMIN-rung exemption and a fail-closed empty-org path, so the delta stayed contained.
Two corrections to the ADR's own line references, found during implementation:
PermissionSet.contextVariables was at permission.zod.ts:308, not :282.
- The approval org-scoping seam is
approval-node.ts:147 (and substantively approval-service.ts:748-768 / :967), not :118.
One ADR claim not confirmed: the "existing 60s cache staleness envelope" the Consequences section attributes to the resolver does not exist there — resolveAuthzContext re-runs per request with no TTL. The 60s window belongs to the better-auth session snapshot cache (auth-manager.ts). D2's resolver therefore adds a per-request read, not a cached one; if that cost matters at scale it needs its own decision.
One implementation finding beyond the ADR: the #2937 write post-image check required a non-array payload, so a BULK insert could carry a forged organization_id per row — the same defect one call site down. Closed as part of D5.
Dogfood findings (the acceptance run paid for itself): #3623 (driver-level native tenant scoping collapsed group union reads to active-org equality — fixed in #3631), #3624 (org creation over HTTP 500s with teams enabled: better-auth 1.7 memberCount vs sys_team — CLOSED by #3647, see the 07-27 comment for the full sweep it triggered), cloud#881 (per-org seed replay primary path never engaged — fixed in cloud#884).
Repo split (per cloud ADR-0016 iron rule 强制免费、治理收费, cloud ADR-0081)
This repo (open, framework): all enforcement primitives (code) — shipped in #3559 + #3631; posture activation is entitled (#3570). Scoped-invitation and delegated-admin console UX live open in objectui's app-shell (correction recorded in the 07-27 D8 comment — cloud sells the runtime that entitles the posture, not the console screens).
cloud repo (commercial, @objectstack/organizations): org lifecycle management, grouping/registry UI, cross-org approval templates, master-data distribution management, per-org seed/config replay, org analytics, D13 promotion tooling. Cloud-side tracking issue: objectstack-ai/cloud#874; acceptance/regression harness: objectstack-ai/cloud#880 (apps/ee-group-showcase, DONE).
D12 line — corrected (#3570; ADR amended in #3603): the open-activation reading shipped in #3559 was reviewed by the founder and ruled wrong — it made the stronger multi-org posture free while isolated stayed entitled, inverting ADR-0081 D2, and it opened a silent-degradation hole (OS_TENANCY_POSTURE=group skipped both the enterprise package load and the ADR-0093 D5 fail-fast). #3570 made group an entitlement again: it probes org-scoping exactly like isolated; without the enterprise runtime the posture resolves to single + degraded, and an os serve boot configured for group refuses to start instead of silently running single-org. Iron-rule safety is satisfied by refusing to run an unwalled group boot, not by free activation — open code is not free activation. Both multi-org postures are @objectstack/organizations capability, and the runtime declares WHICH shapes it entitles (supportedPostures, cloud#875) with the open core failing closed on anything undeclared.
Phases
Phase 0 — correctness (open, immediate, independent of D1) — ✅ landed in #3559
Phase 1 — group posture MVP — ✅ COMPLETE
Phase 2 — group product depth
Phase 3 — governance (commercial, cloud repo)
Refs: ADR-0093, ADR-0095, ADR-0090, ADR-0091, ADR-0092, ADR-0049, ADR-0108, ADR-0111; cloud ADR-0016, cloud ADR-0081.
Tracking issue for ADR-0105 (proposed via #3499).
Summary
Group-shaped customers (multi-plant manufacturing, multi-branch retail, holding structures) need one shared database, organization as the membership boundary, group-wide visibility, and cross-org workflows — a middle posture between today's
singleandmulti. The ADR adds agrouptenancy posture with MOAC-style union semantics (organization_id IN accessible_org_ids), engine-enforced, preserving ADR-0095 W1/W2 invariants.Plan evaluation (2026-07-27)
Findings verified against
main: F1 (security-plugin.tssubstring strip — #3539), F2 (organization_adminwildcard VAMA +auto-org-admin-grant.ts— #3540), F3 (PermissionSet.contextVariables,ExecutionContext.rlsMembership,territory.zod.ts— all consumer-less), F4 (ADR-0092 whitelist has one entry). The technical premises held.Design assessment: sound, and it survived implementation. D2 widened the existing Layer 0 predicate from equality to set membership;
computeTenantLayer0Filterwas already a pure function with thePLATFORM_ADMIN-rung exemption and a fail-closed empty-org path, so the delta stayed contained.Two corrections to the ADR's own line references, found during implementation:
PermissionSet.contextVariableswas atpermission.zod.ts:308, not:282.approval-node.ts:147(and substantivelyapproval-service.ts:748-768/:967), not:118.One ADR claim not confirmed: the "existing 60s cache staleness envelope" the Consequences section attributes to the resolver does not exist there —
resolveAuthzContextre-runs per request with no TTL. The 60s window belongs to the better-auth session snapshot cache (auth-manager.ts). D2's resolver therefore adds a per-request read, not a cached one; if that cost matters at scale it needs its own decision.One implementation finding beyond the ADR: the #2937 write post-image check required a non-array payload, so a BULK insert could carry a forged
organization_idper row — the same defect one call site down. Closed as part of D5.Dogfood findings (the acceptance run paid for itself): #3623 (driver-level native tenant scoping collapsed group union reads to active-org equality — fixed in #3631), #3624 (org creation over HTTP 500s with teams enabled: better-auth 1.7
memberCountvssys_team— CLOSED by #3647, see the 07-27 comment for the full sweep it triggered), cloud#881 (per-org seed replay primary path never engaged — fixed in cloud#884).Repo split (per cloud ADR-0016 iron rule 强制免费、治理收费, cloud ADR-0081)
This repo (open, framework): all enforcement primitives (code) — shipped in #3559 + #3631; posture activation is entitled (#3570). Scoped-invitation and delegated-admin console UX live open in objectui's app-shell (correction recorded in the 07-27 D8 comment — cloud sells the runtime that entitles the posture, not the console screens).
cloud repo (commercial,
@objectstack/organizations): org lifecycle management, grouping/registry UI, cross-org approval templates, master-data distribution management, per-org seed/config replay, org analytics, D13 promotion tooling. Cloud-side tracking issue: objectstack-ai/cloud#874; acceptance/regression harness: objectstack-ai/cloud#880 (apps/ee-group-showcase, DONE).D12 line — corrected (#3570; ADR amended in #3603): the open-activation reading shipped in #3559 was reviewed by the founder and ruled wrong — it made the stronger multi-org posture free while
isolatedstayed entitled, inverting ADR-0081 D2, and it opened a silent-degradation hole (OS_TENANCY_POSTURE=groupskipped both the enterprise package load and the ADR-0093 D5 fail-fast). #3570 madegroupan entitlement again: it probesorg-scopingexactly likeisolated; without the enterprise runtime the posture resolves tosingle+degraded, and anos serveboot configured forgrouprefuses to start instead of silently running single-org. Iron-rule safety is satisfied by refusing to run an unwalled group boot, not by free activation — open code is not free activation. Both multi-org postures are@objectstack/organizationscapability, and the runtime declares WHICH shapes it entitles (supportedPostures, cloud#875) with the open core failing closed on anything undeclared.Phases
Phase 0 — correctness (open, immediate, independent of D1) — ✅ landed in #3559
PermissionSet.contextVariablesfrom specspec/src/security/territory.zod.tsgetAuthTables())Phase 1 — group posture MVP — ✅ COMPLETE
single | group | isolatedin the tenancy service (OS_TENANCY_POSTURE; existingmulti→isolated)ExecutionContext.accessible_org_ids; group Layer 0 filterorganization_id IN accessible_org_ids; empty/absent ⇒RLS_DENY_FILTER. Driver-level union scope (DriverOptions.tenantIds) fixed in fix(authz): widen the driver's native tenant scope to the group membership union — ADR-0105 D2 reaches the wire (#3623) #3631 after the dogfood caught the equality collapse (group posture: SQL driver's native tenant scoping collapses union reads to active-org equality (ADR-0105 D2 never reaches the wire) #3623)group/isolated(incl. the bulk-insert hole)validateOrgAxisRedLines, wired into os lint/compile/validate)rlsMembership—IRlsMembershipResolverunderrls-membership-resolverorganization_admin_no_bypass, derived)features.tenancyPosture-keyed; non-group renders unchanged)organization_idnever lands in the victim org, zero custom security code; PLUS the unentitled-state boundary regression (degrade tosingle+degraded; per-posture entitlement narrowing viasupportedPostures)Phase 2 — group product depth
sys_invitationplacement fields via the ADR-0092 whitelist,beforeCreateInvitationissuance gate, idempotent accept-time application) → feat(authz): expose the caller's delegable scope — the read half of the delegated-admin gate (ADR-0090 D12 / ADR-0105 D8) #3674 (GET /security/my-delegable-scope) → fix(security): resolve the issuer's real grants when authorizing invitation placement (ADR-0105 D8) #3695 (issuance resolves the ISSUER's real grants) → feat(auth): give ADR-0105 D8's scope-bounded issuance a caller —delegated_admin, capped (#3697) #3722 (delegated_adminorg role + invitation role cap — closes ADR-0105 D8: the scope-bounded issuance path has no caller — every principal who may invite is already a tenant admin #3697's no-caller gap AND its escalation chain) → fix(security): governsys_memberwrites — membership is not a delegable capability (#3697 follow-up) #3767 (sys_memberjoinsGOVERNED_OBJECTS); console feat(console): scoped-invitation placement — invite straight into a unit and positions (framework ADR-0105 D8) objectui#2868 + feat(console): makedelegated_adminreachable and narrow both role pickers (framework#3697) objectui#2891; e2e objectstack-ai/cloud#886. The membership-role capability channel placement replaces is closed by ADR-0108 (feat(auth)!: membership grade is not a capability channel — close thesys_member.rolevocabulary (ADR-0108, #3723) #3802)organization:$root/$parentsymbols + slug over D6's tree; legality = shares aparent_organization_idroot, independent of the submitter; non-grouppostures refuse at runtime; ADR text amended in-PR) + fix(approvals): refuse cross-org targeting on directory-less approver types (ADR-0105 D9) #3873 (directory-less approver typesuser/field/managerrefuse instead of staying silently inert — found by the cloud dogfood, not review). Cloud e2e confirmed the two links unit tests structurally can't: union-wall reads by the cross-org approver, anddecide()by a foreign-org actorPhase 3 — governance (commercial, cloud repo)
Refs: ADR-0093, ADR-0095, ADR-0090, ADR-0091, ADR-0092, ADR-0049, ADR-0108, ADR-0111; cloud ADR-0016, cloud ADR-0081.