Skip to content

Blueprints HTTP API

This is the reference for the /v1/blueprints HTTP surface. It maps each operation to its OpenAPI schema, ReBAC gate, audit emission, and the closed Problem.code taxonomy. The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract. The catalog carries two write operations alongside the reads: registering a Blueprint (platform-level gate) and publishing an immutable version under it (per-Blueprint publish gate). Bulk import from an external OCI catalog source lives on the separate blueprint-catalogs surface.

Operations

MethodPathOperation IDReBAC gateAudit relationBody cap
GET/v1/blueprintsListBlueprintsper-row blueprint#read filterblueprint.list (granted, with post-filter item_count)n/a
GET/v1/blueprints/{id}GetBlueprintblueprint#read (BEFORE persistence read)blueprint.getn/a
GET/v1/projects/{id}/blueprintsListProjectBlueprintsproject#read on the path Project (BEFORE persistence read), then the per-row blueprint#read filterblueprint.list on project:<uuid> (granted, with post-filter item_count)n/a
POST/v1/blueprintsRegisterBlueprintplatform#manage (BEFORE the service call, so an unauthorised caller never produces an outbox row)blueprint.register (+ BlueprintRegistered outbox event → async blueprint:<id>#owner@<registrar> tuple)1 MiB
POST/v1/blueprints/{id}/versionsPublishBlueprintVersionblueprint#publish (owner-derived; the post-register grant is eventually consistent)blueprint.publish (+ BlueprintVersionPublished outbox event)1 MiB
  • ListBlueprints.limit is clamped at the handler to [1, 200] with default 50.
  • ListBlueprints.cursor is opaque, HMAC-signed by the server through the CursorCodec port; a tampered cursor surfaces as 400 invalid_cursor. A cursor minted by user A and replayed by user B surfaces as 403 cursor_binding_mismatch.
  • ListBlueprints layers a per-row blueprint#read ReBAC check on top of the slug-ordered persistence window — the response page is the subset the caller is authorised to see. The next_cursor is set whenever the persistence layer returned a full page regardless of how many rows the per-row filter dropped, so a thin authorised cohort still pages forward. The OpenAPI prose names the gate blueprint#user for the ubiquitous-language reader role; the underlying SpiceDB permission is read (owner + publisher + reader).
  • GetBlueprint runs the blueprint#read ReBAC check before the persistence read, so an unauthorised caller receives 403 without the existence side-channel a "load-then-check" flow would leak. The response includes the published versions array; each version carries a typed parameter_schema (a closed set of BlueprintParameter rows the wizard renders).
  • ListProjectBlueprints is ListBlueprints read through one Project: the same slug-ordered window, the same per-row blueprint#read filter, the same cursor contract. Each row gains provider_kinds (the union across the Blueprint's published versions) and provisionable, and the page gains reachable_provider_kinds. A project#read check on the path Project runs before any persistence read, so a Project the caller cannot read is indistinguishable from one that does not exist.

The provisionable verdict

A Blueprint is provisionable for a Project when the union of provider_kinds across its published versions intersects the Project's reachable_provider_kinds.

The reachable set is derived server-side: the Clouds the Project holds an approved Cloud Assignment for, each Cloud's provider mapped onto the Blueprint provider kind naming the same substrate. Three rules follow from that derivation:

  • requested, rejected and revoked assignments contribute nothing. An approved assignment counts whether or not it is materialised, the same rule the provider installer applies, so the offer never promises a substrate the platform will not install.
  • A Cloud on a provider the correspondence does not recognise contributes nothing rather than its raw provider string. No Blueprint can target that substrate, so admitting it would produce a true that CreateResource then refuses.
  • A Blueprint with no published version carries provider_kinds: [] and provisionable: false, as a 200 row rather than an error.

Blueprints that are not provisionable stay in the response on purpose. The Blueprint's accepted kinds beside the Project's reachable kinds name the exact gap — the Blueprint needs gcp, the Project reaches aws, so the next step is requesting a gcp Cloud. Hiding those rows would report "no such template", which is a different and wrong message.

The verdict is assignment-level. A true does not promise an assigned credential for the matching Cloud; provisioning still checks the credential path and still refuses an incompatible pairing with 422 blueprint_provider_mismatch.

Path & query parameters

OperationParameterTypeRequiredNotes
GetBlueprintid (path)string (uuid)yesUUIDv7. Non-zero. Malformed → 400 invalid_blueprint_id.
RegisterBlueprintbodyBlueprintCreateRequestyesKebab-case slug, non-empty display_name; a fresh Blueprint is status: active. Duplicate slug → 409 blueprint_slug_conflict. 201 carries Location: /v1/blueprints/{id}.
PublishBlueprintVersionid (path)string (uuid)yesParent Blueprint (UUIDv7). Missing parent → 404 blueprint_not_found.
PublishBlueprintVersionbodyBlueprintVersionCreateRequestyesprovider_kinds, injection_strategy, parameter_schema, and the XRD/Composition pair are validated BEFORE any persistence write. Re-published (blueprint, version)409 blueprint_version_exists.
ListBlueprintscursor (query)stringnoOpaque HMAC-signed continuation. Tampered → 400 invalid_cursor.
ListBlueprintslimit (query)integerno[1, 200], default 50. Out-of-range → 400 invalid_limit.
ListProjectBlueprintsid (path)string (uuid)yesProject (UUIDv7). Zero or malformed → 400 invalid_project_id. A Project the caller cannot read → 403, whether or not it exists.
ListProjectBlueprintscursor (query)stringnoSame opaque HMAC-signed continuation as ListBlueprints.
ListProjectBlueprintslimit (query)integerno[1, 200], default 50. Out-of-range → 400 invalid_limit.

Schemas

The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:

  • Response: BlueprintResponse (single, with embedded versions: BlueprintVersionResponse[]), BlueprintList (paged).
  • Embedded: BlueprintVersionResponse (publication metadata plus the typed parameter_schema), BlueprintParameter (one row per parameter: name, type, required, default).
  • Project-scoped: ProjectBlueprintOffer (one catalogue entry seen from inside a Project), ProjectBlueprintList (paged, plus the Project's reachable substrates).

The Blueprint response carries the resolved id, slug, display_name, optional description, status (closed enum: active, retired), the optional owning domain_id (or null for catalogue-wide entries), the created_at / updated_at lifecycle timestamps, and the versions array. The shape is shared by every read surface (ListBlueprints, GetBlueprint) so clients only need one binding — the difference between the two surfaces is the population of the versions array, not the envelope.

BlueprintVersionResponse

Each Blueprint version is keyed for humans by its parent Blueprint and its version string. It carries:

  • id — surrogate identifier (UUIDv7) of this immutable version. It is the stable machine handle a Resource references as blueprint_version_id when it is provisioned, so resource create consumes this value rather than the version string.
  • version — non-empty string, unique within the parent Blueprint.
  • provider_kinds — non-empty closed-set array of infrastructure substrates this version can target: aws, azure, gcp, hetzner, openstack.
  • injection_strategy — closed-set discriminator naming how the version threads request parameters into the rendered Composite Resource: cloud-init-user-data, helm-values, provider-secret.
  • parameter_schema — array of typed parameter declarations. May be empty when the version declares no parameters.
  • created_at — version creation timestamp (UTC).

The Crossplane XRD and Composition manifests are storage-internal and are deliberately not exposed on this read surface.

BlueprintParameter

A single typed parameter declaration:

  • name — non-empty parameter name an operator fills in at provisioning time.
  • type — closed scalar type: boolean, integer, string. Object and array types are intentionally not modelled; the Crossplane rendering path accepts only the closed scalar taxonomy.
  • required — whether the operator MUST supply a value.
  • default — optional default value applied when a non-required parameter is omitted. Present only when the parameter declares one; its JSON type matches type. Absent for required parameters and for optional parameters with no declared default.

ProjectBlueprintOffer

A catalogue entry seen from inside one Project. It carries id, slug, display_name, optional description, status (active, retired), created_at and updated_at — the same identity fields BlueprintResponse carries — plus two the platform-scoped shape has no Project to compute:

  • provider_kinds — the union of the substrates the Blueprint's published versions accept, deduplicated and sorted ascending, over the same closed enum: aws, azure, gcp, hetzner, openstack. Empty when the Blueprint has no published version.
  • provisionable — whether provider_kinds intersects the Project's reachable_provider_kinds.

The versions array is not part of this shape. The offer answers whether a Blueprint can be provisioned at all; fetch the Blueprint to choose a version.

ProjectBlueprintList

The paged offer response:

  • items — the ProjectBlueprintOffer rows in this page, after the per-row blueprint#read filter.
  • reachable_provider_kinds — the substrates the Project reaches, deduplicated and sorted ascending, over the same closed enum. Always present; an empty array means the Project holds no approved assignment whose Cloud provider the correspondence knows, and every item is then provisionable: false. Every page repeats it, since it is a property of the Project rather than of the window.
  • next_cursor — set whenever the persistence layer returned a full page, regardless of how many rows the per-row filter dropped.

Error taxonomy

All error responses use the shared Problem envelope (application/problem+json). The 403 path uses the richer PermissionDenied shape carrying the ReBAC denial reason, relation_path, and request correlation_id.

CodeStatusWhereMeaning
invalid_blueprint_id400path-id familyMalformed UUID or zero UUID.
invalid_project_id400ListProjectBlueprintsMalformed or zero Project UUID in the path.
invalid_cursor400List / ListProjectBlueprintsHMAC verification failed.
invalid_limit400List / ListProjectBlueprintsOut of [1, 200].
unauthenticated401every operationRequest carries no authenticated principal.
cursor_binding_mismatch403List / ListProjectBlueprintsCursor presented by a different caller than the one that minted it; per-(caller, pepper) HMAC binding rejected the replay.
blueprint_not_found404Get / PublishBlueprintVersionNo Blueprint with the given {id}. The pre-persistence gate returns 403; this surface returns 404 only after the gate has passed and the row was not found.
blueprint_slug_conflict409RegisterA Blueprint with the same kebab-case slug already exists.
blueprint_version_exists409PublishVersionThe (blueprint, version) pair was already published; versions are immutable.
blueprints_not_provisioned501every operationComposition root did not wire the Blueprint Catalog port (non-production builds only).
internal500every operationServer-side failure path.

A 403 response carries Problem.code = permission_denied plus the extended PermissionDenied fields documented in ./authz.md.

Audit emissions

The Blueprint Catalog application service does not emit audit rows on reads — emission is a transport-layer concern. The handlers stamp:

  • blueprint.list on the granted list path, with the post-filter item_count recorded in the caveat context, and on every denial path. ListProjectBlueprints stamps the same relation with the path Project as its object (project:<uuid>), so an auditor tells a project-scoped offer read from a platform catalogue list by the object rather than by the verb.
  • blueprint.get on the granted get path and on every denial path.

A read whose per-row filter dropped a row for a reason other than a plain denial also records blueprint.list.authz_errors in the caveat context. The row is dropped either way, so the cohort fails closed; the count is how an operator sees that it happened.

Every denial row carries outcome = permission_denied and the missing_relation caveat naming the gate that refused. Invariant rejections (malformed id, missing row) stamp outcome = invariant_violation.

Cross-references

  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the ListBlueprints, GetBlueprint and ListProjectBlueprints operations and the BlueprintResponse / BlueprintList / BlueprintVersionResponse / BlueprintParameter / ProjectBlueprintOffer / ProjectBlueprintList schemas.
  • ../../../internal/transport/http/v1/blueprints/ — the transport-tier implementation: the handlers, the closed Problem.code taxonomy, the limit-clamp constants, and the per-row visibility filter both list paths share.
  • ../../../schema/authz.zed — ReBAC schema; the blueprint definition declares the read permission this surface gates on, and the project definition the read the offer's top-level gate runs.
  • ./cloud-assignments.md — the surface that decides which Clouds a Project holds approved, and therefore which substrates the offer reports as reachable.
  • ./resources.md — companion surface for the Tenancy Resource aggregate; the Resource Create Wizard consumes the BlueprintVersion's parameter_schema and posts a provisioned-flow ResourceCreateRequest.