# Registries API

7 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /registries

List registries

Bring-your-own (external) and managed registries. Credentials are write-only and never returned.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (ListOutputBody3)
  - `items` (RegistryView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## POST /registries

Add an external registry

Adds a registry that takes basic auth (GHCR, Docker Hub, GitLab, Harbor, self-hosted). Credentials are sealed and write-only. ECR, GAR and ACR tokens that expire are not supported. The managed registry is set up with POST /platform-services (kind registry). Requires the admin scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.

Request body (application/json, required): CreateInputBody2
  - `host` (string, required): registry.example.com or registry.example.com:5000, without a scheme or path. ghcr.io, docker.io, registry.gitlab.com, a Harbor or self-hosted host.
  - `name` (string, required): Unique name in the team.
  - `namespace` (string): The path between the host and the repository name: a GitHub user or organization, a Docker Hub namespace, a GitLab group/project. Images are pushed to <host>/<namespace>/<project>-<service>.
  - `pullPassword` (string): Write-only, never returned.
  - `pullUsername` (string): Optional read-only credential the target servers pull with. Without it they use the push credential.
  - `pushPassword` (string, required): Write-only: a token or password, never returned.
  - `pushUsername` (string, required): Write-only credential used to push images (basic auth). A registry without authentication takes any pair.

Response 201: Created (RegistryView)
  - `createdAt` (string (date-time), required)
  - `hasPullAuth` (boolean, required): A separate read-only credential is set; targets pull with it. Otherwise they use the push credential.
  - `hasPushAuth` (boolean, required)
  - `host` (string, required): registry.example.com[:port], no scheme.
  - `id` (string, required)
  - `imageCount` (integer (int64), required): Live images.
  - `inUse` (boolean, required): A team, project or service setting uses it: it cannot be removed.
  - `insecure` (boolean, required): A loopback host: plain http is used.
  - `kind` (enum: external | managed, required)
  - `lastCheck` (map of RegistryCheck, required): Result of the last check, by server id.
  - `manageRetention` (boolean, required): NebulaCtrl deletes images that fall out of the rollback window (managed registries; opt-in for external).
  - `managed` (RegistryManagedView)
  - `name` (string, required)
  - `namespace` (string, required)
  - `platformServiceId` (string)
  - `pullUsername` (string)
  - `pushUsername` (string)
  - `references` (RegistryReferences, required)
  - `sizeBytes` (integer (int64), required): Space the live images take.
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /registries/{registryId}

Get a registry

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `registryId` (path, string, required)

Response 200: OK (RegistryView)
  - `createdAt` (string (date-time), required)
  - `hasPullAuth` (boolean, required): A separate read-only credential is set; targets pull with it. Otherwise they use the push credential.
  - `hasPushAuth` (boolean, required)
  - `host` (string, required): registry.example.com[:port], no scheme.
  - `id` (string, required)
  - `imageCount` (integer (int64), required): Live images.
  - `inUse` (boolean, required): A team, project or service setting uses it: it cannot be removed.
  - `insecure` (boolean, required): A loopback host: plain http is used.
  - `kind` (enum: external | managed, required)
  - `lastCheck` (map of RegistryCheck, required): Result of the last check, by server id.
  - `manageRetention` (boolean, required): NebulaCtrl deletes images that fall out of the rollback window (managed registries; opt-in for external).
  - `managed` (RegistryManagedView)
  - `name` (string, required)
  - `namespace` (string, required)
  - `platformServiceId` (string)
  - `pullUsername` (string)
  - `pushUsername` (string)
  - `references` (RegistryReferences, required)
  - `sizeBytes` (integer (int64), required): Space the live images take.
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /registries/{registryId}

Update a registry

Omitted fields stay as they are; an omitted password keeps the stored one. A managed registry can only be renamed and have its retention switched.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `registryId` (path, string, required)

Request body (application/json, required): PatchInputBody1
  - `clearPull` (boolean): Remove the read-only credential.
  - `host` (string)
  - `manageRetention` (boolean): Let NebulaCtrl delete images that fall out of the rollback window. Managed registries only.
  - `name` (string)
  - `namespace` (string)
  - `pullPassword` (string): Omit to keep the stored password.
  - `pullUsername` (string)
  - `pushPassword` (string): Omit to keep the stored password.
  - `pushUsername` (string)

Response 200: OK (RegistryView)
  - `createdAt` (string (date-time), required)
  - `hasPullAuth` (boolean, required): A separate read-only credential is set; targets pull with it. Otherwise they use the push credential.
  - `hasPushAuth` (boolean, required)
  - `host` (string, required): registry.example.com[:port], no scheme.
  - `id` (string, required)
  - `imageCount` (integer (int64), required): Live images.
  - `inUse` (boolean, required): A team, project or service setting uses it: it cannot be removed.
  - `insecure` (boolean, required): A loopback host: plain http is used.
  - `kind` (enum: external | managed, required)
  - `lastCheck` (map of RegistryCheck, required): Result of the last check, by server id.
  - `manageRetention` (boolean, required): NebulaCtrl deletes images that fall out of the rollback window (managed registries; opt-in for external).
  - `managed` (RegistryManagedView)
  - `name` (string, required)
  - `namespace` (string, required)
  - `platformServiceId` (string)
  - `pullUsername` (string)
  - `pushUsername` (string)
  - `references` (RegistryReferences, required)
  - `sizeBytes` (integer (int64), required): Space the live images take.
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /registries/{registryId}

Delete an external registry

409 `registry_in_use` while a team default, a project or a service override uses it. A managed registry is removed with its platform service (409 `managed_registry`). Images already pushed stay in the registry.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `registryId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`registry_in_use`, `managed_registry`); 422 Unprocessable Entity; 500 Internal Server Error

## POST /registries/{registryId}/check

Test a registry from a server

The agent on the chosen server contacts the registry with the stored credentials and reports whether push and pull are accepted. Waits up to 25 seconds; if the agent has not answered by then the response has `pending: true` and the result appears in the registry's `lastCheck`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `registryId` (path, string, required)
- `serverId` (query, string): The server whose agent runs the check. Builds and pulls reach the registry from servers, so this is the test that matters.

Response 200: OK (RegistryCheck)
  - `at` (string (date-time), required)
  - `error` (string)
  - `ok` (boolean, required)
  - `pending` (boolean): The agent has not answered within the wait; read the registry again for the result.
  - `pull` (boolean, required)
  - `push` (boolean, required)
  - `serverId` (string)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /registries/{registryId}/images

List the images pushed to a registry

Newest first. `live` images are inside the rollback window, `expired` ones are outside it (a managed registry deletes them at the next sweep), `deleted` ones are gone.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `registryId` (path, string, required)
- `state` (query, enum: live | expired | deleted): Only images in this state.
- `limit` (query, integer (int64)): Page size (default 50).
- `cursor` (query, string)

Response 200: OK (RegistryImagePage)
  - `items` (RegistryImageView[] | null, required)
  - `nextCursor` (string)
Errors: 400 Bad Request; 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error
