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 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: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"

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

The 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/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 doingErase 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: