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 the published versions and two fields 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.
  • versions — the Blueprint's published versions, oldest first, each a ProjectBlueprintOfferVersion with the id a Resource create names as blueprint_version_id and the version label. Always present; [] when the Blueprint has no published version. It carries no manifests or parameter schema; fetch the Blueprint for those.
  • provisionable — whether provider_kinds intersects the Project's reachable_provider_kinds.

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.