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 is read-only on the wire — operators publish new Blueprints and BlueprintVersions through a separate management path that is out of scope here.
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 |
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).
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| GetBlueprint | id (path) | string (uuid) | yes | UUIDv7. Non-zero. Malformed → 400 invalid_blueprint_id. |
| 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. |
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).
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 by its parent Blueprint and its version string. It carries:
version— non-empty string, unique within the parent Blueprint.provider_kinds— non-empty closed-set array of infrastructure substrates this version can target:aws,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.
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_cursor | 400 | List | HMAC verification failed. |
invalid_limit | 400 | List | Out of [1, 200]. |
unauthenticated | 401 | every operation | Request carries no authenticated principal. |
cursor_binding_mismatch | 403 | List | 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 | No Blueprint with the given {id}. The pre-persistence blueprint#read gate returns 403; this surface returns 404 only after the gate has passed and the row was not found. |
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.blueprint.geton the granted get path and on every denial path.
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; theListBlueprintsandGetBlueprintoperations and theBlueprintResponse/BlueprintList/BlueprintVersionResponse/BlueprintParameterschemas.../../../internal/transport/http/v1/blueprints/— the transport-tier implementation: the two handlers, the closedProblem.codetaxonomy, the limit-clamp constants, and the per-row visibility filter onListBlueprints.../../../schema/authz.zed— ReBAC schema; theblueprintdefinition declares thereadpermission this surface gates on../resources.md— companion surface for the Tenancy Resource aggregate; the Resource Create Wizard consumes the BlueprintVersion'sparameter_schemaand posts a provisioned-flowResourceCreateRequest.