Appearance
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
| Method | Path | Operation ID | ReBAC gate | Audit relation | Body cap |
|---|---|---|---|---|---|
| GET | /v1/blueprints | ListBlueprints | per-row blueprint#read filter | blueprint.list (granted, with post-filter item_count) | n/a |
| GET | /v1/blueprints/{id} | GetBlueprint | blueprint#read (BEFORE persistence read) | blueprint.get | n/a |
| GET | /v1/projects/{id}/blueprints | ListProjectBlueprints | project#read on the path Project (BEFORE persistence read), then the per-row blueprint#read filter | blueprint.list on project:<uuid> (granted, with post-filter item_count) | n/a |
| POST | /v1/blueprints | RegisterBlueprint | platform#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}/versions | PublishBlueprintVersion | blueprint#publish (owner-derived; the post-register grant is eventually consistent) | blueprint.publish (+ BlueprintVersionPublished outbox event) | 1 MiB |
ListBlueprints.limitis clamped at the handler to[1, 200]with default50.ListBlueprints.cursoris opaque, HMAC-signed by the server through theCursorCodecport; a tampered cursor surfaces as400 invalid_cursor. A cursor minted by user A and replayed by user B surfaces as403 cursor_binding_mismatch.ListBlueprintslayers a per-rowblueprint#readReBAC check on top of the slug-ordered persistence window — the response page is the subset the caller is authorised to see. Thenext_cursoris 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 gateblueprint#userfor the ubiquitous-language reader role; the underlying SpiceDB permission isread(owner + publisher + reader).GetBlueprintruns theblueprint#readReBAC check before the persistence read, so an unauthorised caller receives403without the existence side-channel a "load-then-check" flow would leak. The response includes the publishedversionsarray; each version carries a typedparameter_schema(a closed set ofBlueprintParameterrows the wizard renders).ListProjectBlueprintsisListBlueprintsread through one Project: the same slug-ordered window, the same per-rowblueprint#readfilter, the same cursor contract. Each row gainsprovider_kinds(the union across the Blueprint's published versions) andprovisionable, and the page gainsreachable_provider_kinds. Aproject#readcheck 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,rejectedandrevokedassignments 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
truethatCreateResourcethen refuses. - A Blueprint with no published version carries
provider_kinds: []andprovisionable: false, as a200row 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
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| GetBlueprint | id (path) | string (uuid) | yes | UUIDv7. Non-zero. Malformed → 400 invalid_blueprint_id. |
| RegisterBlueprint | body | BlueprintCreateRequest | yes | Kebab-case slug, non-empty display_name; a fresh Blueprint is status: active. Duplicate slug → 409 blueprint_slug_conflict. 201 carries Location: /v1/blueprints/{id}. |
| PublishBlueprintVersion | id (path) | string (uuid) | yes | Parent Blueprint (UUIDv7). Missing parent → 404 blueprint_not_found. |
| PublishBlueprintVersion | body | BlueprintVersionCreateRequest | yes | provider_kinds, injection_strategy, parameter_schema, and the XRD/Composition pair are validated BEFORE any persistence write. Re-published (blueprint, version) → 409 blueprint_version_exists. |
| ListBlueprints | cursor (query) | string | no | Opaque HMAC-signed continuation. Tampered → 400 invalid_cursor. |
| ListBlueprints | limit (query) | integer | no | [1, 200], default 50. Out-of-range → 400 invalid_limit. |
| ListProjectBlueprints | id (path) | string (uuid) | yes | Project (UUIDv7). Zero or malformed → 400 invalid_project_id. A Project the caller cannot read → 403, whether or not it exists. |
| ListProjectBlueprints | cursor (query) | string | no | Same opaque HMAC-signed continuation as ListBlueprints. |
| ListProjectBlueprints | limit (query) | integer | no | [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 embeddedversions: BlueprintVersionResponse[]),BlueprintList(paged). - Embedded:
BlueprintVersionResponse(publication metadata plus the typedparameter_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 asblueprint_version_idwhen it is provisioned, soresource createconsumes this value rather than theversionstring.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 matchestype. 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— whetherprovider_kindsintersects the Project'sreachable_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— theProjectBlueprintOfferrows in this page, after the per-rowblueprint#readfilter.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 thenprovisionable: 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.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_blueprint_id | 400 | path-id family | Malformed UUID or zero UUID. |
invalid_project_id | 400 | ListProjectBlueprints | Malformed or zero Project UUID in the path. |
invalid_cursor | 400 | List / ListProjectBlueprints | HMAC verification failed. |
invalid_limit | 400 | List / ListProjectBlueprints | Out of [1, 200]. |
unauthenticated | 401 | every operation | Request carries no authenticated principal. |
cursor_binding_mismatch | 403 | List / ListProjectBlueprints | Cursor presented by a different caller than the one that minted it; per-(caller, pepper) HMAC binding rejected the replay. |
blueprint_not_found | 404 | Get / PublishBlueprintVersion | No 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_conflict | 409 | Register | A Blueprint with the same kebab-case slug already exists. |
blueprint_version_exists | 409 | PublishVersion | The (blueprint, version) pair was already published; versions are immutable. |
blueprints_not_provisioned | 501 | every operation | Composition root did not wire the Blueprint Catalog port (non-production builds only). |
internal | 500 | every operation | Server-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.liston the granted list path, with the post-filteritem_countrecorded in the caveat context, and on every denial path.ListProjectBlueprintsstamps 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.geton 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; theListBlueprints,GetBlueprintandListProjectBlueprintsoperations and theBlueprintResponse/BlueprintList/BlueprintVersionResponse/BlueprintParameter/ProjectBlueprintOffer/ProjectBlueprintListschemas.../../../internal/transport/http/v1/blueprints/— the transport-tier implementation: the handlers, the closedProblem.codetaxonomy, the limit-clamp constants, and the per-row visibility filter both list paths share.../../../schema/authz.zed— ReBAC schema; theblueprintdefinition declares thereadpermission this surface gates on, and theprojectdefinition thereadthe 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'sparameter_schemaand posts a provisioned-flowResourceCreateRequest.