Appearance
Label your first Project
Build in your first Domain created a Project; Grant access through a Group granted a relation with rebac tuple add. This lesson combines both: you will define a Label, grant the two relations that authorize assigning it, attach it to a Project, and see how Labels exist to be selected. By the end you will know the difference between a Label definition and an assignment, why assignment is a privileged act, and where the qualified key comes from.
This lesson takes about twenty minutes.
Before you start
You need a running, logged-in stack from Set up your local plexsphere, and you should have finished Build in your first Domain (you recognise a Project) and Grant access through a Group (you recognise a ReBAC relation and rebac tuple add). You also need jq on your $PATH to read fields out of the JSON responses.
Recreate the shell environment and capture the Domain and your own user id:
bash
export PATH="$PWD/bin:$PATH"
export PLEXSPHERE_URL=http://localhost:8080
DOMAIN_ID=$(plexctl domain get acme-corp --output json | jq -r '.id')
USER_ID=$(plexctl whoami --output json | jq -r '.subject')Labels are not new to this Domain even before you define one. Every plexsphere install seeds three immutable platform-scoped Labels — platform/origin (whether a Resource was provisioned or adopted), platform/mesh-ip (a Node's mesh address), and platform/domain (the owning Domain's slug, stamped on every subordinate object). You cannot edit or delete them; they are the registry the platform itself relies on. What you add below is a Label of your own, at Domain scope.
Step 1 — Create a Project to label
A Label is attached to an object, so make a fresh Project to hold one:
bash
PROJECT=$(plexctl project create \
--domain "$DOMAIN_ID" \
--slug reporting \
--display-name "Reporting" \
--output json)
PROJECT_ID=$(echo "$PROJECT" | jq -r '.id')
echo "$PROJECT" | jq '{id, domain_id, slug, name}'json
{
"id": "019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5f",
"domain_id": "019ecc65-8622-7a7f-bfe1-0344c7a22dbd",
"slug": "reporting",
"name": "Reporting"
}Step 2 — Define a Label
A Label has two halves: a definition — the schema, scoped to the platform, a Domain, or a Project — and an assignment — a value attached to one object. Define one at Domain scope. --type enum constrains the allowed values, --applicable-kinds project restricts what it can be attached to, and --on-delete cascade means deleting the definition later removes it and its assignments together:
bash
DEF=$(plexctl label define create \
--scope domain --scope-id "$DOMAIN_ID" \
--key env --type enum --enum-values production,staging \
--applicable-kinds project --on-delete cascade \
--output json)
DEF_ID=$(echo "$DEF" | jq -r '.id')
echo "$DEF" | jq '{id, scope, qualified_key, applicable_kinds, on_delete}'json
{
"id": "019ecc6a-2b3c-7d4e-8f50-1a2b3c4d5e6f",
"scope": "domain",
"qualified_key": "acme-corp/env",
"applicable_kinds": [
"project"
],
"on_delete": "cascade"
}Note the qualified_key: the key you assign and delete by is acme-corp/env — the scope's slug joined to your bare env — not env alone. That prefix is how the same key can exist independently at platform, Domain, and Project scope without colliding. A value type other than enum is just as valid — string (with a max length), numeric (with min/max bounds), boolean, or regex (a pattern the value must match); whichever you choose is what the value in Step 4 is checked against.
Step 3 — Authorize the assignment
Now the twist. Assigning a Label is a privileged act: plexsphere deliberately does not let a Domain admin assign Labels just because they administer the Domain. Assignment needs two explicit relations — maintainer on the target Project and assigner on the Label definition — and neither is granted by the admin relation you hold. Grant them to yourself with the rebac tuple add from the previous lesson. First, maintainer on the Project:
bash
plexctl rebac tuple add \
--project "$PROJECT_ID" \
--resource "project:$PROJECT_ID" \
--relation maintainer \
--subject "user:$USER_ID"text
ID SUBJECT RELATION RESOURCE CREATED_AT
019ecc6a-3c4d-7e5f-9061-2b3c4d5e6f70 user:019ecc65-9f3a-7c14-8d2b-5f3e0a7b1c40 maintainer project:019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5f 2026-06-15T18:10:09ZThen assigner on the Label definition:
bash
plexctl rebac tuple add \
--project "$PROJECT_ID" \
--resource "labeldefinition:$DEF_ID" \
--relation assigner \
--subject "user:$USER_ID"text
ID SUBJECT RELATION RESOURCE CREATED_AT
019ecc6a-4d5e-7f60-8172-3c4d5e6f7081 user:019ecc65-9f3a-7c14-8d2b-5f3e0a7b1c40 assigner labeldefinition:019ecc6a-2b3c-7d4e-8f50-1a2b3c4d5e6f 2026-06-15T18:10:22ZIf a grant returns
Permission Deniedon the first try, wait a couple of seconds and run it again. The Project you created in Step 1 takes a moment to propagate to the authorization backend, and it is that Project'smanagepermission that authorizes the write.
The --project flag names the Project whose manage permission authorizes you to write the tuple — your Domain admin relation already grants that. This is the platform's authorization model made visible: a Label is not a permission, and holding one — or being able to administer a Domain — never silently confers the right to attach Labels to things.
Step 4 — Assign the Label
With both relations granted, attach the Label to the Project with a value the definition allows:
bash
plexctl label object set "project:$PROJECT_ID" \
--definition-id "$DEF_ID" \
--value productiontext
OBJECT_KIND OBJECT_ID QUALIFIED_KEY VALUE ASSIGNED_BY
project 019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5f acme-corp/env production userIf this returns
request denied by ReBAC (reason=insufficient_relation)on the first try, wait a couple of seconds and run it again. The grants you wrote in Step 3 take a moment to propagate; the retry succeeds once they have.
The value is checked against the definition: production is one of the enum values you declared, so it is accepted. Try --value bogus and the call is rejected — the schema you set in Step 2 is enforced at assignment time, not merely documented.
Step 5 — Read the assignment back
List the Labels attached to the Project:
bash
plexctl label object list "project:$PROJECT_ID"text
OBJECT_KIND OBJECT_ID QUALIFIED_KEY VALUE ASSIGNED_BY
project 019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5f acme-corp/env production userThere is your assignment, addressed by its qualified key, carrying the value you set, with ASSIGNED_BY recording that a user — you — attached it.
Step 6 — Labels exist to be selected
A Label you can only read back would be a sticky note. The point of Labels is selection: other parts of plexsphere — network-policy targeting, bulk actions, observability queries, credential assignment — pick objects by Label rather than by id. Selectors support equality (acme-corp/env=production), inequality (!=), set membership (in (production, staging)), and existence (exists / absent), combined with a comma for AND. Parse-test one without touching any object:
bash
plexctl label selector preview --selector "acme-corp/env=production"The command echoes the parsed selector — a single equality term over acme-corp/env — or, if you mistype the grammar, points at the character where parsing failed. A selector that matches your Project is the bridge from "I tagged one thing" to "I can act on every thing tagged this way." The full grammar lives in the Labels bounded context.
Step 7 — Clean up
Deleting the definition removes it and — because you set --on-delete cascade — its assignments in one move. (The other policies are block, which refuses while assignments exist, and orphan, which leaves them readable but unanchored.)
bash
plexctl label define delete --id "$DEF_ID" --yesThe acme-corp/env assignment on your Project goes with it; the seeded platform/* Labels, being immutable, are untouched.
What you learned
- A definition is a schema; an assignment is a value. You define a Label once — at platform, Domain, or Project scope — then attach values to many objects.
- The qualified key namespaces by scope.
acme-corp/envis the scope slug joined to your key, so the same key never collides across scopes. - Values are validated. The
enum/string/numeric/boolean/regextype you declare is enforced at assignment time. - A Label is not a permission. Assigning one is gated by explicit
maintainer+assignerrelations that Domain administration does not confer — so a tag can never quietly escalate access. - Labels exist to be selected. Their payoff is letting policy, actions, and observability pick objects by Label rather than by id.
Where to go next
- Keep learning by doing — Erase an identity from the audit log is the capstone: honour a right-to-erasure request against the hash-chained audit log, then prove the chain still verifies end to end.
Or pick the quadrant that matches what you need now:
- You have a job to do — the manage Label definitions and assign object Labels how-to guides cover the operator runbooks.
- You want the exact contract — the
plexctl labelreference documents every subcommand, flag, and output shape. - You want to understand why Labels are shaped this way — the Labels bounded context covers scopes, value schemas, the selector grammar, and the assignment authorization matrix.