API (v1)¶
The contract is internal/api/openapi.yaml in the repository (OpenAPI 3.0.3); this page is its readable
form. /v1 is a contract: breaking changes only under /v2. A test (openapi_test.go) checks that every error code the
server can emit is documented in the specification.
Transport and authentication¶
- HTTPS only, TLS 1.2 or newer, on
listen.port(default8443). The certificate comes from the chart'stls.secretName; consumers pin the CA that issued it. Authorization: Bearer <token>: one token perConsumer. A missing, malformed or unknown token, a disabled Consumer, a Consumer whose token Secret is outside the release namespace, and a hash configured on more than one Consumer all produce the same401withWWW-Authenticate: Bearer. The cause is never revealed.- The
401path is not rate-limited. There is no lockout or throttling of failed attempts; the token comparison is constant-time against every stored hash, so response time does not depend on how close a guess is. Security rests on high-entropy tokens (openssl rand -hex 32). Authentication is checked before the HTTP method, so an unauthenticated405is never visible. - Isolation. A consumer sees only its own databases. Another consumer's database is indistinguishable from a
non-existent one:
404 not_found, never403. - Every response, including errors,
401,/healthzand/readyz, carriesX-Provider-Version: <semver>. - Responses carry
Cache-Control: no-store. - Error bodies are always
{"error": {"code": "<snake_case>", "message": "..."}}. Branch oncode, never onmessage.5xxis retryable. - Request bodies are limited to 16 KiB, must be one JSON object, and unknown fields are rejected.
externalId¶
The consumer's stable reference (for example an instance UID), matched against ^[A-Za-z0-9._-]{1,128}$.
It is percent-decoded, matched literally, and %2F is not a path separator. . and .. match the pattern but
many HTTP clients normalise them away: avoid them. Invalid: 400 invalid_external_id.
Endpoints¶
| Method and path | Purpose | Success |
|---|---|---|
PUT /v1/databases/{externalId} |
idempotent create-or-get | 201 created, 200 existed |
GET /v1/databases/{externalId} |
phase and, once Ready, the connection | 200 |
DELETE /v1/databases/{externalId} |
request a soft delete | 202 |
GET /v1/databases |
list the caller's databases (never with credentials) | 200 |
GET /healthz |
liveness, no token | 200 |
GET /readyz |
cache synced, no token | 200 / 503 not_ready |
PUT: create or get¶
curl --cacert ca.crt -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"engine":"postgres","sizeClass":"small","labels":{"app":"nextcloud"}}' \
https://<provider>:8443/v1/databases/inst-1234
{"externalId":"inst-1234","engine":"postgres","phase":"Provisioning","shard":null,"connection":null}
- Body fields (all optional):
engine(onlypostgres),sizeClass(a name from the install'ssizeClasses),labels(at most 32 entries; keys^[A-Za-z0-9]([A-Za-z0-9._/-]{0,61}[A-Za-z0-9])?$, values printable, at most 256 bytes). - Concurrent
PUTs for oneexternalIdcreate exactly one database (one201). sizeClassis create-time only. The body of aPUTon an existing database is not applied; the response describes the database as it is. There is no resize in v1.- Unknown
sizeClass:400 unknown_size_class(absent or empty uses the install default). PUTon a database whose deletion was requested (phaseDeleting) is409 database_deleting. The provider never silently revives it. Use a newexternalId, or wait until the database is gone, after which the id can be reused.- Quota: the Consumer's
maxDatabasesreached gives429 quota_exceeded. - The database is created asynchronously: poll
GETuntilphaseisReady.
GET: phase and connection¶
{"externalId":"inst-1234","engine":"postgres","phase":"Ready","shard":"shard01",
"connection":{"host":"192.0.2.200","port":5432,"database":"t_abc...","user":"t_abc...","password":"...","sslmode":"require"}}
connection is non-null only when phase is Ready. Credentials stay retrievable while Ready, so a lost
consumer-side Secret needs no reset. After a delete is requested, the phase is Deleting immediately and
connection is null. 503 credentials_unavailable: Ready, but the credentials are not readable yet, not yet applied on the shard (a password change in progress), or the shard's client host is invalid; retry. The provider never serves a password the shard does not have.
shard is null until the database has been placed.
Phases: Provisioning, Ready, Deleting, Deleted, Failed (Failed = no shard capacity, retried by the
provider).
DELETE: soft delete¶
Returns 202 with the database (phase Deleting or Deleted, connection: null). The provider revokes login
and terminates sessions at once and drops the database and role after the grace period (deletion.gracePeriod,
default 7 days) counted from the provider's first observation of the request. DELETE is idempotent:
repeating it, also while Deleting or Deleted, returns 202 and does not move the deadline. An unknown
externalId is 404. See Recover a soft-deleted database.
Errors¶
| Status | code |
When |
|---|---|---|
| 400 | invalid_external_id, invalid_body, unsupported_engine, unknown_size_class, invalid_labels |
request shape |
| 401 | unauthorized |
any authentication failure (generic) |
| 404 | not_found |
unknown externalId, or another consumer's database |
| 405 | method_not_allowed |
wrong method (checked after authentication; with Allow) |
| 409 | database_deleting |
PUT on a database being deleted |
| 409 | external_id_conflict |
an object not belonging to the caller occupies the name (manual intervention only) |
| 413 | body_too_large |
body over 16 KiB |
| 429 | quota_exceeded |
the Consumer's maxDatabases reached |
| 503 | credentials_unavailable, unavailable, not_ready |
transient; retry |
| 500 | internal_error |
unexpected server fault; retry |
Versioning¶
Read X-Provider-Version from any response to learn the provider's version, for example to refuse to run
against a provider older than the feature set you need.