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

No one assigned

    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