Skip to content

ApproverType.role is the last surface projecting the reserved word — the ADR-0090 D3 exception it relies on has a false premise #3133

Description

@os-zhuang

Recording a finding surfaced while fixing the check:role-word ratchet failure on main (#3120). No code change proposed here — this is a decision record for later.

TL;DR

ApproverType.role (packages/spec/src/automation/approval.zod.ts) is the last place the platform projects better-auth's org-membership tier under the reserved word "role". It is not covered by ADR-0090 D3's exception — and the sentence that would cover it asserts something that never happened.

The D3 exception has a false premise

ADR-0090 D3 (docs/adr/0090-permission-model-v2-concept-convergence.md:202-204):

Single documented exception: the better-auth boundary — sys_member.role is third-party schema we do not own; it remains, already relabelled org_membership_level in the platform projection (ADR-0057 D7), and its UI label is "organization membership", never "role".

The exception's logic is: the third-party column is stuck with the word, but everything the platform surfaces is org_membership_level — the word never reaches an author. That premise does not hold:

Claim Reality
ADR-0090:203 — "already relabelled" org_membership_level appears once in the whole repo, as a comment in packages/spec/src/identity/position.zod.ts:25. Zero implementation.
ADR-0057 D7 (0057-...md:335) Explicitly lists it under "Deferred (evidence-gated, P4)": sys_member.roleorg_membership_level relabel

ADR-0090 states as complete a relabel that ADR-0057 deferred and that no code implements. Because the projection never landed, the word leaked from the third-party column up onto our own authoring surface.

Why ApproverType.role isn't covered by the exception

The exception protects better-auth's column (sys_member.role) — a thing we cannot rename. ApproverType is our own enum, in our own spec, that an author types into a flow node. It is the projection of that concept onto our authoring surface, and per the ADRs the projection is named org_membership_level, labelled "organization membership", never "role".

Every other surface already converged (ADR-0090 D3):

  • sys_role / sys_user_rolesys_position / sys_user_position
  • ShareRecipientType.roleposition ✅ (enum is now ['user','group','position','unit_and_subordinates','guest'])
  • ctx.roles[] / current_user.rolepositions / position
  • ApproverType.role → still role ❌ — the sole hold-out

It has no real users, and it costs us a lint rule

Repo-wide scan for type: 'role': only lint tests, docs warnings, and CHANGELOG entries. Zero examples, zero showcase, zero real callers.

Meanwhile it is a proven trap with a real incident behind it — packages/lint/src/validate-approval-approvers.ts exists specifically to catch it, and its rule table marks the origin "hotcrm class":

After ADR-0090 D3 renamed sys_rolesys_position, downstream apps that authored { type: 'role', value: 'sales_manager' } silently route the approval to nobody

The failure is silent: expansion finds no sys_member row → falls back to the role:sales_manager literal (expandApprovers pushes `${a.type}:${a.value}` for every type) → the request waits forever on an approver that cannot exist.

Anecdotally, this mental model ("role was deprecated, replaced by position — so ApproverType.role must be the legacy half") is exactly what a maintainer reached for unprompted while reviewing #3120. The name manufactures the confusion.

Per ADR-0077's principle — the authoring surface should make the mistake unwritable rather than lint it after the fact — a lint rule that exists to disambiguate a name is a smell pointing at the name.

Suggested shape (for whoever picks this up)

Rename, don't remove: roleorg_membership_level, matching the projection label the ADRs already mandate.

  • Removing would drop a legitimate (if rare) capability: routing to owner/admin/member.
  • Renaming kills the trap dead — nobody accidentally writes { type: 'org_membership_level', value: 'sales_manager' }; the name refuses the mistake, and approval-role-not-membership-tier can be deleted.
  • It closes out the deferred ADR-0057 P4 relabel and corrects ADR-0090:203's false statement in the same pass.

Cost: breaking change to a published spec enum (15.1 is out), so unlike D3's "one-step renames, no aliases" this needs a deprecation window + a major.

Refs

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions