Appearance
Domains HTTP API
This is the reference for the /v1/domains HTTP surface. It maps each operation to its OpenAPI schema, ReBAC gate, audit emission, outbox event, 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 — for operator recipes see ../../how-to/identity/manage-domains.md; for the bounded-context narrative (slug-immutability decision, mesh-CIDR cross-Domain non-overlap, ReachabilityPolicy default substitution, empty-aggregate-on-delete contract, mesh_cidr_invalidates_subrange retarget guard) see ../../contexts/identity/tenancy.md. For the cursor-paginated list idiom and the canonical Problem.code style this surface inherits, see ./projects.md — Domains and Projects are the two tenancy CRUD surfaces and share the same shape.
Operations
| Method | Path | Operation ID | ReBAC gate | Audit relation | Outbox event | Body cap |
|---|---|---|---|---|---|---|
| POST | /v1/domains | CreateDomain | platform domain#create | domain.create | DomainCreated | 8 KiB |
| GET | /v1/domains | ListDomains | per-row domain#read filter | (none on success; per-row denials emitted) | (none) | n/a |
| GET | /v1/domains/{id} | GetDomain | domain#read (BEFORE persistence read) | domain.read | (none) | n/a |
| PATCH | /v1/domains/{id} | PatchDomain | domain#manage | domain.update (NAMES-only fields_changed) | DomainUpdated | 8 KiB |
| DELETE | /v1/domains/{id} | DeleteDomain | domain#manage | domain.delete | DomainDeleted | n/a |
body_cap = 8 KiBis enforced before the JSON decoder runs; an over-cap body surfaces as413 request_body_too_large.ListDomains.limitis clamped at the handler to[1, 200]with default50.ListDomains.cursoris opaque, HMAC-signed by the server; a tampered cursor surfaces as400 invalid_cursor.GetDomainruns thereadReBAC check before the persistence read, so an unauthorised caller receives403without the existence side-channel a "load-then-check" flow would leak.PatchDomainrejects bodies that carry aslugkey (even with the same value) at decode time with400 slug_immutable— the slug is the URL handle exported into cached dashboard links and outbox projections, and mutating it would silently rot every cached reference.PatchDomaindoes acceptregion, in contrast to the immutable slug: re-pinning a Domain to a different region is a supported PATCH and is the trigger that drives the management fleet to migrate the Domain's Projects. An empty-stringregionclears the pin (unpins the Domain); a non-empty value must match the kebab-case pattern and the 64-byte ceiling, otherwise the aggregate rejects it with400 invalid_domain.PatchDomainrequires the body to set at least one ofname,description,mesh_cidr,region, orreachability— an empty body surfaces as400 empty_patch.DeleteDomainruns the empty-aggregate guard inside the same transaction as the row delete; at least one persisted Project, Group, Identity, IdP binding, or Node forces409 domain_not_emptywith the structuredDomainChildCountspayload in the Problem'schild_countsextension so the operator knows which sub-aggregate to drain first.
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| GetDomain / PatchDomain / DeleteDomain | id (path) | string (uuid) | yes | UUIDv7. Non-zero. Malformed → 400 invalid_domain_id. The DomainID parameter component is shared across all three operations. |
| ListDomains | cursor (query) | string | no | Opaque HMAC-signed continuation. Tampered → 400 invalid_cursor. |
| ListDomains | 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:
- Request:
DomainCreateRequest,DomainPatchRequest. - Response:
DomainResponse(single),DomainList(paged). - Embedded:
ReachabilityPolicy(heartbeat + stale + unreachable durations),DomainChildCounts(delete-conflict extension).
The Domain response carries the resolved id, name, slug, description, mesh_cidr, region, reachability, and lifecycle timestamps. The shape is shared by every read surface (CreateDomain, GetDomain, ListDomains, PatchDomain) so clients only need one binding.
The region field is an optional deployment-locality handle on DomainCreateRequest, DomainPatchRequest, and DomainResponse: a string matching pattern: ^[a-z0-9]+(-[a-z0-9]+)*$ with maxLength: 64. An absent or empty region means the Domain is unpinned and its Projects are placed freely; a non-empty value pins the Domain so the management fleet schedules each of its Projects onto a cluster whose region exactly matches. Unlike slug, region is present on DomainPatchRequest and may be re-set after create — see the patch note above and the management-fleet reference for how a re-pin drives placement migration.
A fully-zero reachability policy on CreateDomain is replaced by the platform default at the aggregate; a partial policy (some durations zero, others set) is rejected with 400 invalid_reachability_policy rather than silently filled in, because partial fills almost always indicate a misconfigured client.
Mesh-CIDR invariants
The mesh_cidr field carries three distinct invariants enforced at three distinct layers:
| Layer | Invariant | Failure code |
|---|---|---|
| Aggregate | Canonical RFC 4632 form (no host bits set, no IPv6 zone). | 400 invalid_domain |
| SQL GIST exclusion | No two persisted Domains' mesh_cidrs overlap. | 409 mesh_cidr_overlap |
| Service-level retarget guard | A PatchDomain retarget that would orphan an existing project_mesh_ip_reservations.sub_range is refused. | 422 mesh_cidr_invalidates_subrange (with the offending project_id in the detail) |
The retarget guard runs even when the new mesh_cidr is a strict superset of the old — a sub-range that fits the old CIDR but not the new one (e.g. shrinking from /16 to /22) is the exact case it catches.
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, traversed relation_path, and request correlation_id.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_domain_id | 400 | path-id family | Malformed UUID. |
invalid_domain | 400 | Create / Patch | Aggregate invariant rejected the body (empty Name, malformed Slug, malformed mesh_cidr). |
invalid_reachability_policy | 400 | Create / Patch | Partial reachability body. |
slug_immutable | 400 | Patch | Body carried a slug key. |
empty_patch | 400 | Patch | Body had no patchable fields set. |
invalid_cursor | 400 | List | HMAC verification failed. |
invalid_limit | 400 | List | Out of [1, 200]. |
domain_not_found | 404 | path-id family | No Domain with the given {id}. |
domain_slug_conflict | 409 | Create | Slug already taken by another Domain. |
mesh_cidr_overlap | 409 | Create / Patch | SQL GIST exclusion caught the overlap. |
domain_not_empty | 409 | Delete | Domain still has child aggregates; payload carries DomainChildCounts. |
request_body_too_large | 413 | Create / Patch | Body exceeded the 8 KiB tenancy ceiling. |
mesh_cidr_invalidates_subrange | 422 | Patch | Retarget would orphan an existing Project sub-range. |
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.
Cross-references
../../contexts/identity/tenancy.md— bounded-context reference for theDomain → Project → Resource → Nodeaggregate hierarchy, the slug-immutability decision, the ReachabilityPolicy default substitution, themesh_cidr_invalidates_subrangeretarget guard, and the empty-aggregate-on-delete contract../projects.md— sibling tenancy CRUD surface for Projects, with the same cursor/limit pagination idiom and the samePermissionDeniedshape../identities.md,./invitations.md— per-Domain child-aggregate read surfaces.../../how-to/identity/manage-domains.md— operator how-to for creating, patching, and deleting Domains.../../how-to/platform/bootstrap-domains.md— bootstrap recipe for the very first Domain on a fresh deployment.../../../api/openapi/plexsphere-v1.yaml— OpenAPI 3.1 spec; the*Domain*operations and theDomainCreateRequest/DomainPatchRequest/DomainResponse/DomainList/ReachabilityPolicy/DomainChildCountsschemas.../../../internal/identity/tenancy/— the bounded-context implementation: theDomainaggregate, the empty-aggregate guard, and the mesh-CIDR retarget service.