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-out none → gets organization_id none → gets organization_id
provenance managedBy: 'config', admins extend in Setup managedBy: '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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions