Skip to content

docs(configure): define the org-membership tier vocabulary on the users page - #245

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary
Aug 29, 2026
Merged

docs(configure): define the org-membership tier vocabulary on the users page#245
os-zhuang merged 1 commit into
mainfrom
claude/issue-153-org-membership-tier-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #153

The four-value org-membership tier vocabulary (owner, admin, delegated_admin,
member — ADR-0108, sys_member.role) was used on two pages and defined on none.
A reader who met delegated_admin had nowhere in the corpus to go, and the nearest
thing they would reach for — "Delegated administration" on the permission-sets page —
is a different mechanism.

Where the section went, and why

content/docs/configure/users.mdx, in a new ## Organization-membership tiers
section. Three reasons, all checkable:

  1. configure/users.mdx:12 already lists the objects the page covers and sys_member
    is among them. The page had claimed the object whose role field this is and then
    never defined its tiers — an omission on an existing owner page, not a new home.
  2. configure/permissions/ documents what a principal may do. Upstream is explicit
    that this tier is not that (objectstack:packages/spec/src/identity/membership-role.ts):
    "It carries NO ObjectStack authority by construction … Role = can reach the
    endpoint
    ; adminScope = what the endpoint permits." A tier that confers no
    authority, filed inside the authority model, invites the exact misreading the card
    exists to prevent.
  3. The same file states the placement principle: "the other two are rules that belong
    near what they govern." Near what it governs = near sys_member.

Position within the page: between "Add people" and "Place people in the org tree
(memberships)".
The tier is chosen at admission — it is a value on the invitation the
preceding section just described — so this is where the chronology puts it. It also
puts the two senses of "membership" side by side, which is where a reader conflates
them, so the distinction is drawn at the seam instead of pages apart. It is deliberately
not folded into the existing memberships section: that section is
sys_business_unit_member, a different object, and merging the two would merge two of
the three facts the spec names as "look like one — do not merge them".

What the section says, and what each claim was measured against

Everything below is read off objectstack origin/main, not inferred from the name:

Claim Source
The vocabulary is closed and platform-owned; nothing widens it at boot ADR-0108 D1; membership-role.ts BUILTIN_MEMBERSHIP_ROLE_OPTIONS
owner may invite at any tier invitation-role-cap.ts orgRoleGrade (owner = 3, the ceiling)
owner is the only tier that may remove another owner better-auth's removeMember predicate, quoted in member-role-canonical.ts
admin may invite at any tier except owner invitationRoleCapFailure (requested grade may not exceed the issuer's) plus better-auth's creatorRole check
delegated_admin may issue invitations without being an org admin, and that reach is the whole of it MEMBERSHIP_ROLE_DELEGATED_ADMIN doc comment; isOrgAdminGrade returns false for it
An invitation may never confer a tier above the issuer's own; an issuer below admin may invite only as member invitationRoleCapFailure, both refusal branches
An app's own business role cannot be stored in either field, and an invitation naming one is rejected before any row is written ADR-0108 D2 (ROLE_NOT_FOUND at better-auth's door)
owner/admin memberships are auto-granted an organization-admin permission set scoped to that organization plugin-security/src/auto-org-admin-grant.ts

That last row is why the "grade, not a bundle" line can be stated without hand-waving:
it names where the visible power of owner/admin actually comes from (a permission
set, on the ordinary permission path), so the tier is not left looking like the source
of it.

delegated_admin was the value most at risk of being written wrong, so it gets its own
subsection rather than a table cell. Its "on its own" row is the narrow, measured claim
lets you invite a plain member, and place nobody — rather than anything the name
suggests: the invitation cap holds a below-admin issuer to plain member, and placement
authority comes solely from a separately-granted adminScope. Nothing about "what a
delegated admin may do" is claimed beyond what those two files enforce.

The two consuming pages

configure/notifications.mdx and build/automation/approvals.mdx now link to the new
section. These are link-only edits: no word is added or removed on either page —
the diff is a Markdown link wrapped around text that was already there, plus a re-wrap
so the lines stay under the files' existing width. The tier enumerations stay in place;
they are useful where they are, and removing them would have been the prose rewrite the
card ruled out.

Verification

All runs on the final commit 5c7c4a1, from the repo root, exit codes captured before
any pipe:

Check Result
pnpm turbo run build type-check test --force Tasks: 3 successful, 3 total, wrapper exit 0
node .github/scripts/check-locale-surface.mjs exit 0 — "every advertised URL has a source file and every source file is advertised"
node apps/docs/scripts/gen-zh-hant.mjs --check exit 0 — "73 generated file(s) match the zh-Hans sources byte for byte"
node .github/scripts/check-translation-ownership.mjs exit 0 — "touches 0 translation artifact(s) and 3 other file(s)"
node .github/scripts/check-translations.mjs exit 0 — "translations gate passed"
node .github/scripts/check-translation-output.mjs --self-test exit 0
node .github/scripts/check-node-floor.mjs exit 0

The locale-surface oracle is unchanged, and that is positive evidence. The gate
reports 79 logical pages over 8 locales = 397 docs entries. Adding a section adds no
page and drops none, and the claim is measured rather than asserted: the gate's oracle
inputs are the content/docs/**/*.mdx path set plus each file's frontmatter title:,
and that (path, title) set is byte-identical between b0b159b and 5c7c4a1 — 397
pairs on both sides, diff exit 0
. apps/docs/lib/i18n.ts is untouched, and
git diff --diff-filter=ADR over the range is empty.

Anchors resolve — checked against the built HTML, not assumed. No gate here catches
a broken in-page anchor. In .next/server/app/en/docs/configure/users.html:
id="organization-membership-tiers", id="delegated_admin-is-not-delegated-administration"
and id="place-people-in-the-org-tree-memberships" all present; the new
href="/docs/configure/users#organization-membership-tiers" appears once in the built
notifications page and twice in approvals; and the outbound
permission-sets#delegated-administration target exists in that page's built HTML. The
slug oracle was confirmed independently: github-slugger@2.0.0 reproduces the existing
#layer-1--identity link on this very page from its heading text.

The rendered section was read back out of the built HTML: two tables, nine rows, no raw
pipes — a malformed MDX table degrades to a paragraph silently and no gate catches that
either.

Scope

No changeset (this repo has none). No configure/permissions/ restructuring;
permission-sets.mdx is untouched (it was read in full, not as a snippet, because the
new section links into it). No locale siblings. No new page.

Possible follow-up, deliberately not done here because it is inside the section the card
put out of scope: the sys_member row of the Layer 1 identity table in
configure/permissions/index.mdx could also link to the new section.


Generated by Claude Code

…rs page

`sys_member.role`'s four tiers (`owner`, `admin`, `delegated_admin`,
`member` — ADR-0108) were used on two pages and defined on none, so a
reader who met `delegated_admin` had nowhere in the corpus to go.

Add a dedicated "Organization-membership tiers" section to
`configure/users.mdx` — the page that already lists `sys_member` among
the objects it covers. The section gives each tier a one-line meaning,
states that a tier is a grade deciding what you can reach rather than a
bundle of what you may do, separates `delegated_admin` from the
`adminScope` mechanism documented as "Delegated administration" on the
permission-sets page, and separates `sys_member` from the
`sys_business_unit_member` org-tree placement the page already covers.

The two consuming pages (`configure/notifications.mdx`,
`build/automation/approvals.mdx`) now link to it. Those are link-only
edits: no word is added or removed, only re-wrapped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] No page defines the org-membership tier vocabulary — delegated_admin is addressable in two places and explained in none

2 participants