Skip to content
🛡️ adminadmin@example.com · Domain admin in Acme Corp

Chapter 3 — Read a tenant

What can I see

bash
plexctl domain list
text
ID                                    SLUG       DISPLAY_NAME  CREATED_AT
019ecc65-8622-7a7f-bfe1-0344c7a22dbd  acme-corp  Acme Corp     2026-06-15T17:47:46Z

Four Domains exist. You see one.

This is not a filter someone remembered to add to a query. The list surface asks the authorization engine which Domains this principal holds a relation in, and returns those. Chapter 5 shows the engine directly.

Look inside

bash
plexctl project list --domain "$DOMAIN_ID"

Projects are where work lives — each one owns Resources and gets a slice of the Domain's mesh range.

bash
plexctl identity list --domain "$DOMAIN_ID"

Identities are per-Domain. This list is Acme Corp's, and there is no list that spans Domains.

The audit log is not an add-on

bash
plexctl audit entries list --domain "$DOMAIN_ID"

Every state change in this Domain, hash-chained.

You have written nothing yet, so what you are looking at is the seed's own trail — the bootstrap Job creating Domains and binding identity providers, recorded the same way your commands will be.

Chapter 4 writes into this and reads its own writes back.

Now walk into the wall

Beta LLC is not discoverable through the API — the Domain list already showed you that. So take its id straight from the database and point a call at it while still signed in as your Acme Corp principal:

bash
DOMAIN_ID_BETA=$(kubectl exec statefulset/postgres -- \
  env PGPASSWORD=plexsphere psql -U plexsphere -d plexsphere -tAc \
  "SELECT id FROM plexsphere.domains WHERE slug='beta-llc'")
plexctl identity list --domain "$DOMAIN_ID_BETA"
text
plexctl: Permission Denied: identities ListIdentities: caller lacks domain:read on the addressed Domain

Changing --domain changed the target. It did not change who you are.

Two surfaces, two refusals

bash
plexctl project list --domain "$DOMAIN_ID_BETA"
text
ID  DOMAIN  SLUG  DISPLAY_NAME  CREATED_AT

A header and nothing under it. Exit code 0.

The identity surface denied. The Projects surface filtered — row by row, showing nothing it may show, without confirming the Domain exists at all.

Different signal, same guarantee. A script that automates plexsphere has to treat "empty" and "denied" as the same wall.

Ask the engine directly

bash
USER_ID=$(plexctl whoami --output json | jq -r '.subject')

plexctl rebac check \
  --subject "user:$USER_ID" \
  --relation read \
  --resource "domain:$DOMAIN_ID"
text
DECISION  REASON  CORRELATION_ID
allowed           c9663519-57c6-4e68-92f5-cad6e81f9b2a

REASON stays empty — the engine only fills it in to explain a denial.

The same question, the other Domain

bash
plexctl rebac check \
  --subject "user:$USER_ID" \
  --relation read \
  --resource "domain:$DOMAIN_ID_BETA"
text
plexctl: rebac check: denied (reason=insufficient_relation, correlation_id=e64fa71c-870e-4fac-b868-a41e5af60ea8)

insufficient_relation is the reason behind both earlier refusals — the 403 and the empty page.

The tenancy boundary is not a convention layered on top of the API. It is this decision, and every read on the last five slides went through it.

🏢 betaadmin@example.com · a different principal, in Beta LLC

Optional — so where is Beta LLC

Exactly where Acme Corp is. You just have to sign in to it:

bash
plexctl login --domain-id "$DOMAIN_ID_BETA" --profile-name beta
bash
plexctl domain list --profile beta
text
ID                                    SLUG      DISPLAY_NAME  CREATED_AT
019ecc65-9012-7b3c-a1d4-7e2f5a8b0c11  beta-llc  Beta LLC      2026-06-15T17:47:46Z

Beta LLC appears. Acme Corp does not.

The same human, the same Dex account — two principals, two subject UUIDs. One login is one tenant. There is deliberately no cross-tenant super-admin.

Presenter note: this costs a browser sign-in, and it must be a private window — the earlier approval left a session cookie that the approval page will happily reuse, minting an Acme Corp token under the beta profile and making the slide read backwards. Skip when short on time.

Optional — drop the second session

bash
plexctl logout --profile beta
text
Logged out. Cleared profile "beta".

Revokes the Beta token server-side and drops the profile. The default Acme Corp profile is untouched.

🛡️ adminadmin@example.com · Domain admin in Acme Corp

Everything has a machine shape

bash
plexctl domain list --output json

Always { "items": [ … ] }.

And it resolves the ambiguity from four slides ago: when a list is empty because rows were hidden from you, the JSON carries empty_reason: no_rebac_membership. When it is empty because nothing exists, the field is absent.

Its presence, not its value, is the signal.