Skip to content

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 (default 8443). The certificate comes from the chart's tls.secretName; consumers pin the CA that issued it.
  • Authorization: Bearer <token>: one token per Consumer. 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 same 401 with WWW-Authenticate: Bearer. The cause is never revealed.
  • The 401 path 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 unauthenticated 405 is never visible.
  • Isolation. A consumer sees only its own databases. Another consumer's database is indistinguishable from a non-existent one: 404 not_found, never 403.
  • Every response, including errors, 401, /healthz and /readyz, carries X-Provider-Version: <semver>.
  • Responses carry Cache-Control: no-store.
  • Error bodies are always {"error": {"code": "<snake_case>", "message": "..."}}. Branch on code, never on message. 5xx is 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 (only postgres), sizeClass (a name from the install's sizeClasses), 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 one externalId create exactly one database (one 201).
  • sizeClass is create-time only. The body of a PUT on 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).
  • PUT on a database whose deletion was requested (phase Deleting) is 409 database_deleting. The provider never silently revives it. Use a new externalId, or wait until the database is gone, after which the id can be reused.
  • Quota: the Consumer's maxDatabases reached gives 429 quota_exceeded.
  • The database is created asynchronously: poll GET until phase is Ready.

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.