Skip to content

Register a Blueprint

A Blueprint is a reusable recipe; a BlueprintVersion is a concrete, immutable revision of it. This lesson — the first of the three that build the provisioning catalog — registers the official Blueprint catalog as a source scoped to your Acme Corp Domain and imports the aws-s3-bucket Blueprint from it, acting as the Acme Corp domain admin. You then sign in as an ordinary member to confirm the catalog and its blueprints are visible to a normal tenant — not just to privileged authors.

aws-s3-bucket is a standalone (nodeless) recipe: it provisions one AWS S3 bucket — no plexd Node, nothing to enrol — against the demo AWS Cloud, which the lean stack serves through the floci emulator. A Blueprint stands on its own — it needs no Cloud or Credential to be registered — so it leads the provisioning track; the Cloud and Credential lessons that follow build the rest of the catalog, and Provision a cloud Resource will consume all of it.

This lesson takes about ten minutes.

Before you start

This lesson is standalone: it does not depend on the Cloud or Credential lessons. You need a running, logged-in stack from Set up your local plexsphere, jq on your $PATH. Importing from the official catalog pulls a bundle from ghcr.io, so the stack needs outbound network access.

You act as the Acme Corp domain admin through Steps 1–4 — admin@example.com, which holds domain#manage on Acme Corp. Registering a catalog into a Domain is a domain-admin action, so the admin is the right author here (the platform operator, who builds the catalog-global Cloud and Credential in the other two lessons, is not). In Step 5 you switch to member@example.com, an ordinary Acme Corp member with no authoring grant. See the tutorials overview for the full cast.

Recreate the shell environment and read the Acme Corp Domain id with make dev-ids — the sign-in resolves the active IdP binding from it, and you pass it as the catalog's Domain scope:

bash
export PATH="$PWD/bin:$PATH"
export PLEXSPHERE_URL=http://localhost:8080

eval "$(make -s dev-ids)"
echo "domain=$DOMAIN_ID"

Step 1 — Register the official catalog into your Domain

Sign in as the Acme Corp domain admin under the default profile, completing sign-in in the browser as admin@example.com with the password password:

bash
plexctl login --domain-id "$DOMAIN_ID"

Register the official upstream catalog as a source scoped to Acme Corp — the --domain-id flag makes it a Domain-scoped source, gated by the domain#manage grant the admin holds rather than the platform-global grant. The official catalog signs its bundles with a GitHub Actions keyless cosign identity, so you pin the verification policy to its release-workflow signing identity:

bash
CATALOG_ID=$(plexctl blueprint catalog add \
  --name "Official Blueprint catalog" \
  --ref ghcr.io/plexsphere/blueprints:v1 \
  --domain-id "$DOMAIN_ID" \
  --verify-identity '^https://github\.com/plexsphere/blueprints/\.github/workflows/release\.yml@refs/tags/v.*$' \
  --verify-issuer https://token.actions.githubusercontent.com \
  --output json | jq -r '.id')
echo "$CATALOG_ID"
text
019fb9c3-685d-75dd-a6d9-84747e1b0860

The identity pattern accepts only signatures produced by a tagged release run of the catalog's release.yml workflow — a bundle signed off any other ref is rejected. Because the source is scoped to Acme Corp, every Blueprint imported from it is a Domain-scoped Blueprint — that is what makes it readable by Acme Corp's members in Step 5.

Step 2 — Browse the source catalog

Browse the source to see what it offers — nothing is imported yet:

bash
plexctl blueprint catalog browse "$CATALOG_ID"
text
SLUG                       DISPLAY_NAME                    VERSION   PROVIDER_KINDS             INJECTION_STRATEGY
aws-ec2-instance           AWS EC2 Instance                v1alpha1  aws                        provider-secret
aws-eks-cluster-daemonset  AWS EKS Cluster with DaemonSet  v1alpha1  aws                        helm-values
aws-s3-bucket              AWS S3 Bucket                   v1alpha1  aws                        provider-secret
generic-vm                 Generic VM                      v1alpha1  aws,gcp,hetzner,openstack  cloud-init-user-data
hetzner-server             Hetzner Cloud Server            v1alpha1  hetzner                    cloud-init-user-data
kubernetes-cloudless-node  Cloudless Kubernetes Node       v1alpha1  aws                        helm-values
openstack-instance         OpenStack Instance              v1alpha1  openstack                  cloud-init-user-data

Browsing reads the source, not your catalog: every row here is on offer, none of it is yours yet. Two columns describe the recipe rather than name it. PROVIDER_KINDS is the set of substrates the version can target — most are single-substrate, generic-vm spans four. INJECTION_STRATEGY names how the version threads your request parameters into the rendered Composite Resource, one of cloud-init-user-data, helm-values, or provider-secret, and it is fixed per version.

Two of these rows carry the tutorials: aws-s3-bucket is the one you import in Step 3, and kubernetes-cloudless-node is the recipe Reach your Resource provisions a Node from.

To pin down a single row instead of reading the table, ask for JSON and filter it:

bash
plexctl blueprint catalog browse "$CATALOG_ID" --output json \
  | jq -r '.items[] | select(.slug == "aws-s3-bucket") | {slug, version, provider_kinds}'
json
{
  "slug": "aws-s3-bucket",
  "version": "v1alpha1",
  "provider_kinds": ["aws"]
}

Step 3 — Import the aws-s3-bucket Blueprint

Browsing left your catalog untouched. Now take one row off the source and make it yours. The import inherits the source's Domain scope — no flag needed — so the Blueprint lands scoped to Acme Corp. The verifier checks the bundle's signature against the pinned identity before the version is published:

bash
plexctl blueprint import "$CATALOG_ID" aws-s3-bucket
text
SLUG           STATUS    VERSION   REASON
aws-s3-bucket  imported  v1alpha1

The import prints one row per Blueprint it touched: status: imported and the published version confirm the bundle passed verification and its first version is now in the catalog, and an empty REASON means nothing was skipped. The import registers the aws-s3-bucket Blueprint and publishes its first version. Unlike a node blueprint, this one declares a region parameter — the AWS region the bucket is created in — so the resource create in the provisioning lesson passes {"region":"us-east-1"} rather than an empty object.

Step 4 — Read the version id

The import put the Blueprint in your catalog, so it now shows up in a plain listing rather than only in the source you browsed in Step 2:

bash
plexctl blueprint list --all
text
ID                                    SLUG           DISPLAY_NAME   STATUS  CREATED_AT
019fb9c4-192a-7030-9df2-012d53541ae7  aws-s3-bucket  AWS S3 Bucket  active  2026-07-31T20:01:04Z

One row, because one Blueprint was imported. The other six recipes the source offers are still only on offer.

resource create does not take that id, though. It takes the BlueprintVersion's id, the broker's internal handle for one revision, and the listing shows Blueprints rather than their versions. Resolve the Blueprint by slug, then read the version id out of the Blueprint's versions array, which blueprint get surfaces on the read API:

bash
BLUEPRINT_ID=$(plexctl blueprint list --all --output json \
  | jq -r '.items[] | select(.slug == "aws-s3-bucket") | .id')
BLUEPRINT_VERSION_ID=$(plexctl blueprint get "$BLUEPRINT_ID" --output json \
  | jq -r '.versions[] | select(.version == "v1alpha1") | .id')
echo "$BLUEPRINT_VERSION_ID"
text
019fb9c4-1930-7e9c-982f-41807fbff897

A different id from the one in the table above: that one identifies the Blueprint, this one identifies its v1alpha1 revision. BLUEPRINT_VERSION_ID is the unit a Resource is provisioned against, and the provisioning lesson re-derives it the same way, so there is nothing to carry across.

Step 5 — Confirm a normal member sees the catalog

A Domain-scoped catalog is meant to be discoverable by the Domain's ordinary members, not just by the admin who imported it. Confirm that now.

Sign in as member@example.com — a plain Acme Corp member with no authoring grant — and save the session under a named member profile so it does not overwrite the admin token you used above. Complete sign-in in the browser with the password password:

bash
plexctl login --profile-name member --domain-id "$DOMAIN_ID"

Each surface is checked twice, because a list and a by-id read are different gates. A list filters row by row and answers 0 either way, so an absent row and a forbidden row look identical from outside. A by-id read has to commit: it returns the object or it denies. Passing both is what shows the member genuinely holds read, rather than having been handed an empty page.

Start with discovery. Can the member find the source at all, without being told its id?

bash
plexctl blueprint catalog list --profile member --output json \
  | jq -r '.items[] | {id, name, domain_id}'
json
{
  "id": "019fb9c3-685d-75dd-a6d9-84747e1b0860",
  "name": "Official Blueprint catalog",
  "domain_id": "019fb9be-d382-7867-af40-1db88a1e6679"
}

One row, and its domain_id is Acme Corp: this is the Domain-scoped source you registered in Step 1, listed for an account that had no part in registering it. The source's read gate is the Domain's read permission, which every member holds.

Now the by-id read, which cannot hide behind an empty page:

bash
plexctl blueprint catalog get "$CATALOG_ID" --profile member --output json \
  | jq -r '{id, name, status}'
json
{
  "id": "019fb9c3-685d-75dd-a6d9-84747e1b0860",
  "name": "Official Blueprint catalog",
  "status": "active"
}

The same id comes back, now with status: active. The member is reading the source itself, not a listing entry.

The Blueprints imported from that source behave the same way. Read access inherits Domain membership through the Blueprint's parent edge, so no explicit grant was ever made to this member. List first:

bash
plexctl blueprint list --all --profile member --output json \
  | jq -r '.items[] | select(.slug == "aws-s3-bucket") | {id, slug, status}'
json
{
  "id": "019fb9c4-192a-7030-9df2-012d53541ae7",
  "slug": "aws-s3-bucket",
  "status": "active"
}

--all is a paging flag: it follows the continuation cursor until the result set is exhausted, so nothing is missed on a later page. What the pages contain is still only what this principal may read. Then read it by id:

bash
plexctl blueprint get "$BLUEPRINT_ID" --profile member --output json \
  | jq -r '{id, slug, status}'
json
{
  "id": "019fb9c4-192a-7030-9df2-012d53541ae7",
  "slug": "aws-s3-bucket",
  "status": "active"
}

Identical id, identical status: the member reads the same Blueprint the admin imported, not a reduced projection of it.

All four calls answer — the member can discover the recipe. What the member cannot do is author: a blueprint catalog add or blueprint import as --profile member is denied, because publish and manage do not inherit from Domain membership. Read is shared; authoring stays privileged.

Do this in the Console

Every step above has a browser equivalent in the Console. As the domain admin, open the Blueprint catalogs page under the Blueprints sidebar group.

Step 1 (blueprint catalog add) is the Register catalog source button above the table. The dialog carries the same choices as the CLI flags: a name; a registry and repository pinned to a tag or a digest; a verification policy that is either a pinned cosign identity (an identity-SAN regexp plus an OIDC issuer) or explicit insecure-unsigned; a pinned or track-tag tracking policy; a Scope picker; and an optional registry-credential reference. The picker offers only the scopes you may register into: registering a catalog-global (platform-wide) source needs manage on the platform — the Platform Operator's authority — while a Domain-scoped source needs manage on the owning Domain. Signed in as the Acme Corp admin you are therefore offered exactly the Acme Corp scope, already preselected; the catalog-global option only appears for a platform-signed-in operator. To reverse a registration, each row's Deregister button (blueprint catalog rm) sits behind a confirm that spells out that Blueprints already imported from the source are orphaned — kept in the catalog but unlinked — not deleted.

Once a source is registered, click it to browse it — Step 2's blueprint catalog browse. Each offered Blueprint row carries an Import button — Step 3's blueprint import; the per-Blueprint outcome — imported, unchanged, drift, conflict, or error — shows inline on the row, and a genuine import adds the Blueprint to the Blueprints list. Open the imported Blueprint there to read its detail (blueprint get): each published version shows its provider kinds, injection strategy, the version idresource create consumes, and its typed parameter schema.

Then sign in to the Console as member@example.com and open the same two pages: a normal member sees the Domain-scoped catalog under Blueprint catalogs and the imported recipe under Blueprints — read-only, with no Import or version-publish action — the browser equivalent of Step 5.

What you learned

  • A Blueprint is a recipe; a BlueprintVersion is an immutable revision. Importing registers the Blueprint and publishes one version under it; a published version never changes.
  • The catalog starts empty — you fill it from a source. Registering an external OCI catalog and importing from it is the fastest way to bring a Blueprint into the catalog, and the verifier proves the bundle was signed by the identity you pinned before any version is published.
  • A Domain-scoped catalog is readable by the Domain's members. Scoping the source to Acme Corp with --domain-id makes the catalog and every Blueprint imported from it discoverable by ordinary members — read inherits Domain membership — while import and version publishing stay privileged to the domain admin. A catalog registered catalog-global (no --domain-id) stays privileged.
  • The version id is the unit you provision against. resource create takes the immutable BlueprintVersion id — not the Blueprint id — and the parameter schema tells you what input it needs. The aws-s3-bucket version declares a required region.

Where to go next

  • Keep learning by doingCreate a Cloud creates the demo AWS Cloud the catalog provisions against — the next piece of the provisioning catalog, built by the platform operator.

Or pick the quadrant that matches what you need now:

  • You have a job to do — the Author a Blueprint how-to covers the operator runbook for both import and hand-authoring.
  • You want the exact contract — the plexctl blueprint reference documents every flag and output shape.
  • You want to understand why the catalog is shaped this way — the Blueprint Catalog context explains the Blueprint and version aggregates.