# Instance services API

6 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 /admin/instance-services

Built-in services: health, default quota and every team's slice

Instance administrator only (403 `instance_admin_required`). S3 holds NebulaCtrl's own data (backups), the registry the teams' images, Loki their central logs. Each team has a slice of each; slices of deleted teams stay until `purgeAt`. No key, bucket or address is returned.

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

Response 200: OK (InstanceServicesView)
  - `defaultQuotaBytes` (integer (int64), required): The S3 quota of every team without its own
  - `services` (InstanceServiceView[] | null, required)
  - `slices` (SliceView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden (`instance_admin_required`); 500 Internal Server Error

## PUT /admin/instance-services

Set the default S3 quota of the teams

Instance administrator only. The default applies to every team without its own quota (50 GiB until changed); the minimum is 1 GiB (422 `quota_too_small`). Lowering it below what a team already uses only blocks that team's new backups: nothing is deleted. Audited.

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

Request body (application/json, required): UpdateServicesInputBody
  - `defaultQuotaBytes` (integer (int64), required): The S3 quota of every team without its own, in bytes. At least 1 GiB.

Response 200: OK (InstanceServicesView)
  - `defaultQuotaBytes` (integer (int64), required): The S3 quota of every team without its own
  - `services` (InstanceServiceView[] | null, required)
  - `slices` (SliceView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity (`quota_too_small`); 500 Internal Server Error

## POST /admin/instance-services/check

Check the built-in services now

Instance administrator only. Runs the same check as the minute-by-minute one: health of each service, the system bucket, and a slice for every team that lacks one.

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

Response 200: OK (InstanceServicesView)
  - `defaultQuotaBytes` (integer (int64), required): The S3 quota of every team without its own
  - `services` (InstanceServiceView[] | null, required)
  - `slices` (SliceView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## PUT /admin/team-slices/{sliceId}/quota

Set one team's S3 quota

Instance administrator only. `quotaBytes` null resets the team to the instance default. Only the S3 slice has a quota (422 `quota_not_supported`). Lowering it below use only blocks new backups; existing backups are never deleted to make room. Audited.

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

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

Request body (application/json, required): SliceQuotaInputBody
  - `quotaBytes` (integer (int64) | null, required): The team's own quota in bytes, at least 1 GiB; null resets it to the instance default.

Response 200: OK (SliceView)
  - `deletedAt` (string (date-time) | null, required): Set when the team was deleted; the slice is purged at purgeAt
  - `effectiveQuotaBytes` (integer (int64), required): The quota that applies (s3); 0 for kinds without a quota
  - `id` (string, required)
  - `kind` (enum: s3 | registry | loki, required)
  - `message` (string, required): Why the slice is failed; empty otherwise
  - `purgeAt` (string (date-time) | null, required)
  - `quotaBytes` (integer (int64) | null, required): The team's own quota; null = the instance default (s3 only)
  - `state` (enum: provisioning | ready | full | failed, required)
  - `teamId` (string, required)
  - `teamName` (string, required)
  - `usedBytes` (integer (int64), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity (`quota_not_supported`); 500 Internal Server Error

## DELETE /admin/team-slices/{teamId}

Purge a deleted team's slices now

Instance administrator only. Deletes the bucket, its key, the registry images and the logs of a team that was deleted, instead of waiting for the 30 days. `confirm` is the team's name. 409 `team_still_exists` while the team exists, 404 when it has no slices left, 422 `confirmation_mismatch`. Audited.

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

Parameters:
- `teamId` (path, string, required)
- `confirm` (query, string): The deleted team's name, as the slice table shows it.

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`team_still_exists`); 422 Unprocessable Entity (`confirmation_mismatch`); 500 Internal Server Error; 502 Bad Gateway

## GET /team-slice

The team's built-in backup storage, registry and logs

Used space and quota of the team's built-in backup storage, the size of its registry images and the log retention. Never a key, bucket, repository prefix or address: the services are managed by the instance administrator.

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

Response 200: OK (TeamSliceView)
  - `backup` (BackupSliceView, required)
  - `logs` (LogsSliceView, required)
  - `registry` (RegistrySliceView, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error
