Found while implementing objectstack-ai/objectos#120 (documenting the sys_notification_subscription.principal selector, which resolves role: against sys_member.role). Out of that card's scope; filed unassigned. Source read only — nothing measured at runtime.
The two lists
packages/spec/src/automation/approval.zod.ts:139:
/** Org-membership tiers (`sys_member.role`: better-auth's closed set). */
export const ORG_MEMBERSHIP_LEVELS = ['owner', 'admin', 'member'] as const;
packages/spec/src/identity/membership-role.ts:99-104 — "the WHOLE vocabulary, in display order (ADR-0108)":
export const BUILTIN_MEMBERSHIP_ROLES = [
MEMBERSHIP_ROLE_OWNER,
MEMBERSHIP_ROLE_ADMIN,
MEMBERSHIP_ROLE_DELEGATED_ADMIN,
MEMBERSHIP_ROLE_MEMBER,
] as const;
sys_member.role declares options: [...BUILTIN_MEMBERSHIP_ROLE_OPTIONS] (packages/platform-objects/src/identity/sys-member.object.ts:208-214), and that file's own comment calls it "The framework's four roles — the WHOLE list", "ENFORCED on write". sys_invitation.role uses the same option list (sys-invitation.object.ts:224).
Why it matters
APPROVER_VALUE_BINDINGS.org_membership_level (approval.zod.ts:155) sources the approver picker from ORG_MEMBERSHIP_LEVELS, so a delegated_admin membership tier appears to be unaddressable as an approver even though it is a storable, enforced value of the column the approver type names.
The doc comment is also stale on its own terms: it attributes the list to better-auth, but delegated_admin is ObjectStack's own addition (ADR-0105 D8 / #3697), so "better-auth's closed set" is no longer the same set as sys_member.role's.
Two readings — a decision, not a patch
- Deliberate:
delegated_admin must never be an approver tier. Then the constant needs a comment saying so and naming the reason, since today it reads as an out-of-date copy of the vocabulary. - Drift: the picker should offer whatever
BUILTIN_MEMBERSHIP_ROLES holds, and ORG_MEMBERSHIP_LEVELS should be derived from it rather than re-spelled, so the next tier addition cannot silently miss the approver surface.
Docs echo filed separately in ObjectOS (the published approvals page repeats the three-value list).
Found while implementing objectstack-ai/objectos#120 (documenting the
sys_notification_subscription.principalselector, which resolvesrole:againstsys_member.role). Out of that card's scope; filed unassigned. Source read only — nothing measured at runtime.The two lists
packages/spec/src/automation/approval.zod.ts:139:packages/spec/src/identity/membership-role.ts:99-104— "the WHOLE vocabulary, in display order (ADR-0108)":sys_member.roledeclaresoptions: [...BUILTIN_MEMBERSHIP_ROLE_OPTIONS](packages/platform-objects/src/identity/sys-member.object.ts:208-214), and that file's own comment calls it "The framework's four roles — the WHOLE list", "ENFORCED on write".sys_invitation.roleuses the same option list (sys-invitation.object.ts:224).Why it matters
APPROVER_VALUE_BINDINGS.org_membership_level(approval.zod.ts:155) sources the approver picker fromORG_MEMBERSHIP_LEVELS, so adelegated_adminmembership tier appears to be unaddressable as an approver even though it is a storable, enforced value of the column the approver type names.The doc comment is also stale on its own terms: it attributes the list to better-auth, but
delegated_adminis ObjectStack's own addition (ADR-0105 D8 / #3697), so "better-auth's closed set" is no longer the same set assys_member.role's.Two readings — a decision, not a patch
delegated_adminmust never be an approver tier. Then the constant needs a comment saying so and naming the reason, since today it reads as an out-of-date copy of the vocabulary.BUILTIN_MEMBERSHIP_ROLESholds, andORG_MEMBERSHIP_LEVELSshould be derived from it rather than re-spelled, so the next tier addition cannot silently miss the approver surface.Docs echo filed separately in ObjectOS (the published approvals page repeats the three-value list).