Skip to content

sys_position.name is the third instance of the #8323 class: an admin-authored name on a tenant-scoped RBAC object carries an installation-wide unique index #8468

Description

@os-zhuang

Found while implementing #8323 (option 2), sweeping the docs for uniqueness claims at the PM's request. Not fixed there — the ruling of 2026-08-13 named exactly two objects, and this is a third. Filed for triage rather than folded in.

The declaration

packages/plugins/plugin-security/src/objects/sys-position.object.ts:

indexes: [{fields: ['name'],unique: true},{fields: ['active']},],

Structurally identical to the sys_capability instance #8323 just fixed:

sys_capability (fixed in #8461)sys_position
declared index{ fields: ['name'], unique: true }{ fields: ['name'], unique: true }
tenancy opt-outnone → gets organization_idnone → gets organization_id
provenancemanagedBy: 'config', admins extend in SetupmanagedBy: 'config', admins author positions

On a DECLARED index bare unique: true is the positional spelling of 'global' — the listed columns verbatim — so name is an installation-wide key on a tenant-scoped object. That is the #8323 §1 shape exactly: an organization can POST a position name and read 409-vs-201 to learn whether another organization already holds it, while its own read of that name returns nothing.

The reference docs assert the strong reading as though it were intended — content/docs/references/identity/position.mdx: name … "Unique position name (lowercase snake_case)". That line is generated from the field's describe(), so the spec text says installation-unique too.

What I did and did not establish

That intent question is why this is a separate card and not a line in #8461: if per-organization is right the fix is one word plus the same replace_unique_index migration (already generalized to declared indexes in #8461, so the migration machinery is in place and free); if installation-wide is right the correct fix is the opposite — spell it unique: 'global' explicitly so the intent stops being a positional accident, and correct the describe().

Either way the current state is the one spelling ADR-0120 says states no scope at all, on a tenant-scoped object, which unique/unscoped-declared-index already warns about.

Related

I also swept the other platform objects while here. Most bare declared uniques are legitimately installation-wide (sys_session.token, sys_api_key.key, sys_oauth_*.token, sys_device_code.*) and several already hand-write the organization composite (sys_team['name','organization_id'], sys_business_unit['code','organization_id'], sys_member['organization_id','user_id'] — the ADR-0120 S6 legacy spelling, valid indefinitely). sys_position is the one that looked like the fixed pair rather than like either of those groups.

Filed unassigned. Found by session session_012WMpuAfA2KSdDjGF6tm1bH.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions