Skip to content

Commit 4111bdf

Browse files
committed
docs(permissions): positions are membership roles too (#3723)
The positions page said `sys_member.role` was the "org-membership tier: owner/admin/member". That parenthetical was already one value short (`delegated_admin`, #3697) and this branch makes it materially wrong: every declared position / permission-set name is now a valid `sys_member.role` value, so someone can be invited straight into a position. Documents that path where the names are declared, with the two limits that bound it — the invitation role cap (an issuer below admin grade invites plain members only; a delegate's channel for capability is placement, not the membership role) and the machine-name requirement.
1 parent e9b11df commit 4111bdf

1 file changed

Lines changed: 35 additions & 1 deletion

File tree

content/docs/permissions/positions.mdx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ of their own** — they only decide *who gets which sets*.
1616
> security identifiers and labels). The vocabulary is now unambiguous:
1717
> **permission set** = capability, **position** = distribution,
1818
> **business unit** = hierarchy. The single exception is better-auth's
19-
> internal `sys_member.role` (org-membership tier: owner/admin/member).
19+
> internal `sys_member.role` (org-membership tier: `owner`, `admin`,
20+
> `delegated_admin`, `member`). A position name you declare here is also
21+
> registered as a valid `sys_member.role` value, so someone can be invited
22+
> directly into the position — see [Positions as membership
23+
> roles](#positions-as-membership-roles).
2024
2125
```typescript
2226
import type { Position } from '@objectstack/spec/identity';
@@ -86,6 +90,36 @@ is no singular `current_user.position` at runtime). Framework-seeded names:
8690

8791
Test membership in RLS/CEL with `'org_admin' in current_user.positions`.
8892

93+
## Positions as membership roles
94+
95+
Every position and permission-set name your stack declares is registered as a
96+
valid organization role, so an invitation can name it directly:
97+
98+
```http
99+
POST /api/v1/auth/organization/invite-member
100+
{ "email": "new.hire@example.com", "role": "sales_rep" }
101+
```
102+
103+
On acceptance the value lands in `sys_member.role`, and the membership
104+
projection passes an unrecognized value straight through as a position name —
105+
so the invitee holds `sales_rep` in `current_user.positions` and resolves
106+
whatever permission sets are bound to it. Both `sys_invitation.role` and
107+
`sys_member.role` are pickers built from that same registered set, so the
108+
Setup UI offers your names alongside the built-ins.
109+
110+
Two limits are worth knowing before you use this as a provisioning path:
111+
112+
- **It is not a way to hand out more authority than you have.** An issuer
113+
below `admin` grade may invite only as plain `member`, and no invitation may
114+
ever confer a role above the issuer's own. A delegated admin grants
115+
capability through the invitation's *placement* (business unit + positions),
116+
which is authorized against their `adminScope` — not through the membership
117+
role, which nothing scopes.
118+
- **Only spec-compliant names are registered.** A declared name that is not
119+
lowercase snake_case (`/^[a-z][a-z0-9_]*$/`, min 2 characters) is skipped
120+
with a boot warning, and an invitation naming it is refused with
121+
`ROLE_NOT_FOUND`.
122+
89123
## Audience anchors: `everyone` and `guest` (ADR-0090 D5/D9)
90124

91125
Two more built-in positions are **implicit** — they are never stored as

0 commit comments

Comments
 (0)