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 label-demo \
--display-name "Label Demo" \
--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": "label-demo",
"name": "Label Demo"
}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"text
OP KEY VALUE VALUES
eq acme-corp/env productionThe command echoes the parsed selector as a table, one row per clause: OP is the operator (eq here), KEY the qualified key it targets, VALUE the literal for eq/neq, and VALUES the set for an in term — so acme-corp/env in (production, staging) would leave VALUE empty and list both under VALUES, while exists / absent fill neither. A comma joins clauses with AND. Mistype the grammar and the command points at the character where parsing failed instead. 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.
But preview only parses the expression — it never touches an object or reports what matches. To resolve a selector to the objects that carry the matching Labels and that you can access, use plexctl label object search:
bash
plexctl label object search \
--selector "acme-corp/env=production" --relation maintainertext
KIND ID
project 019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5fThat is the Project from Step 1 — the one object that carries acme-corp/env=production and that you hold maintainer on (the grant you wrote in Step 3). The command combines two axes in one call: the Label axis (the selector) and the access axis (the relation). Objects you cannot reach via the relation are silently omitted, so the result is exactly the set you are allowed to act on.
Each subsystem that consumes selectors applies the same Label axis its own way: plexctl policy dry-run evaluates a candidate network policy and returns the Nodes its selector matches, and plexctl action dispatch --selector resolves a selector to a cohort of Nodes and runs an action across it.
The access axis on its own — "which objects do I hold a relation on?", ignoring Labels — is the reverse of the rebac check from Grant access through a Group: plexctl rebac lookup-resources lists them. label object search is what you reach for when you want to filter by both axes at once — the tag and the authorization — without intersecting two lists by hand.
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.
Do this in the Console
Every step above has a browser equivalent in the Console. The Label definitions page (under the Labels sidebar group) lists the Domain's definitions and its Create definition dialog builds one — choosing the value type reveals the matching control (Values for enum, a Pattern for regex, and so on).
Steps 3 to 5 — authorize the assignment, then assign and manage Labels — live together in the Label workbench tool under Tools. Pick the Project, then click Authorize me to assign here: one click writes both the maintainer and assigner grants for you, with the project:<project-id> and labeldefinition:<definition-id> resources derived from your selection — no hand-typed object references. The subject defaults to your own user:<id> (override it to authorize a teammate), the action is safe to re-run, and a denial spells out what you need rather than failing silently. As the lesson's caveat notes, the grants take a moment to take effect, so if your first assignment is denied, wait a moment and try again.
The same workbench lists the Project's Label definitions in scope (each with a Remove action) and hosts the Assign Label / Detach panel, so the whole authorize-then-assign flow stays on one surface. (You can still assign on a Project's detail page: open its row on the Projects page and use the Labels panel.) The Selector preview tool under Tools parses a selector expression and shows its structure or inline syntax errors before you rely on it, and the Find by Label tool beside it runs the label object search from Step 6 in the browser — enter a selector and a relation and it lists the objects you can access.
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 — Give a machine its own identity closes the core track: authenticate a ServiceIdentity with the
client_credentialsgrant, then rotate and revoke its token — the non-interactive counterpart to theplexctl loginyou have used throughout.
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.