Skip to content

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:09Z

Then 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:22Z

If a grant returns Permission Denied on 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's manage permission 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 production
text
OBJECT_KIND  OBJECT_ID                             QUALIFIED_KEY  VALUE       ASSIGNED_BY
project      019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5f  acme-corp/env  production  user

If 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  user

There 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  production

The 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 maintainer
text
KIND     ID
project  019ecc6a-1a2b-7c3d-8e4f-0a1b2c3d4e5f

That 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" --yes

The 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/env is the scope slug joined to your key, so the same key never collides across scopes.
  • Values are validated. The enum / string / numeric / boolean / regex type you declare is enforced at assignment time.
  • A Label is not a permission. Assigning one is gated by explicit maintainer + assigner relations 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 doingGive a machine its own identity closes the core track: authenticate a ServiceIdentity with the client_credentials grant, then rotate and revoke its token — the non-interactive counterpart to the plexctl login you have used throughout.

Or pick the quadrant that matches what you need now: