Skip to content

Tracking: ADR-0105 — group tenancy posture; organization scope as a first-class authorization dimension #3541

Description

@os-zhuang

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

  • D8: scoped invitations — engine seam LANDED (feat(auth): onInvitationAccepted host seam — afterAcceptInvitation forwarded to the host (ADR-0105 D8 prerequisite) #3645: AuthManagerConfig.onInvitationAccepted forwards better-auth's afterAcceptInvitation with the raw invitation/member rows, failure-isolated); remaining: cloud-side issuance UX + placement application over the adminScope anti-escalation gate (cloud#874)
  • D9: cross-org approval targeting (approval-service.ts expandPositionUsers / openNodeRequest are the seams)
  • D10: layered master data — blocked on its follow-up ADR (the ADR reserves the concept only)
  • D13: BU-subtree → organization promotion tool (+ verification harness)
  • "All my organizations" console affordances

Phase 3 — governance (commercial, cloud repo)

  • SoD constraints, certification campaigns (ADR-0091 data), deny/muting layer (ADR-0066 ⑦⑧), generalized dimension security (territory's successor)

Refs: ADR-0093, ADR-0095, ADR-0090, ADR-0091, ADR-0092, ADR-0049; cloud ADR-0016, cloud ADR-0081.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions