Tracking issue for ADR-0105 (proposed via #3499).
Status: Phase 0 and Phase 1 COMPLETE. 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). Phase 2/3 remain open (D8's engine seam already landed, #3645).
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 — OPEN), 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).
cloud repo (commercial, @objectstack/organizations): org lifecycle management, grouping/registry UI, scoped-invitation UX, 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; 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— OPEN), 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).
cloud repo (commercial,
@objectstack/organizations): org lifecycle management, grouping/registry UI, scoped-invitation UX, 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
AuthManagerConfig.onInvitationAcceptedforwards better-auth'safterAcceptInvitationwith the raw invitation/member rows, failure-isolated); remaining: cloud-side issuance UX + placement application over theadminScopeanti-escalation gate (cloud#874)approval-service.tsexpandPositionUsers/openNodeRequestare the seams)Phase 3 — governance (commercial, cloud repo)
Refs: ADR-0093, ADR-0095, ADR-0090, ADR-0091, ADR-0092, ADR-0049; cloud ADR-0016, cloud ADR-0081.