# Platform API

13 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 /platform-services

Platform services: the catalog and what is set up

Services NebulaCtrl runs for its own use (S3 storage, a log store, a tailnet control server, single sign-on), each wired into the integration that asked for it.

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

Response 200: OK (PlatformListing)
  - `kinds` (PlatformKindView[] | null, required)
  - `projectId` (string)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## POST /platform-services

Set up a platform service for me

Stages the service in the team's system project, deploys it, then wires it into the integration (a backup destination, a log drain, the Tailscale integration or a sign-in provider) with generated credentials and tests the connection. Returns 202; poll the service (or listen to events) until `status` is `ready` or `failed`. Not idempotent-keyed: the response of the identity provider carries a one-time password.

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

Request body (application/json, required): ProvisionInputBody
  - `bucket` (string): S3: the bucket (default nebulactrl-backups).
  - `hostname` (string): Public hostname; default a generated one under the server's base domain, else <ip>.sslip.io.
  - `kind` (enum: s3 | loki | grafana | headscale | idp | registry, required): Which platform service to set up. registry: the managed container registry for build servers.
  - `providerName` (string): Single sign-on: the label of the sign-in button (default NebulaCtrl SSO).
  - `publicUrl` (string): Overrides https://<hostname>: a proxy on a non-standard port, or plain http for a local test.
  - `replace` (boolean): Headscale: replace an existing Tailscale integration.
  - `retentionDays` (integer (int64)): Loki: how long logs are kept (default 14).
  - `serverId` (string): The server it runs on (Grafana always runs next to Loki).
  - `variant` (enum: pocketid | authelia): Single sign-on: the product. pocketid (default): passkeys only, people managed in Pocket ID. authelia: password + 2FA, NebulaCtrl manages the people.

Response 202: Accepted (PlatformProvisionBody)
  - `initialPassword` (string): Shown once; reset it from the people list if lost.
  - `initialUser` (string)
  - `service` (PlatformServiceView, required)
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /platform-services/{id}

Get a platform service

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

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

Response 200: OK (PlatformServiceView)
  - `backup` (PlatformBackupView)
  - `createdAt` (string (date-time), required)
  - `details` (map of string, required)
  - `environmentSlug` (string)
  - `hostname` (string, required)
  - `id` (string, required)
  - `kind` (enum: s3 | loki | grafana | headscale | idp | registry, required)
  - `label` (string, required)
  - `message` (string, required): What is happening or why it failed.
  - `projectId` (string)
  - `publicUrl` (string, required)
  - `serverId` (string)
  - `serverName` (string)
  - `services` (PlatformServiceRef[] | null, required)
  - `status` (enum: provisioning | ready | failed | removing, required)
  - `step` (enum: configure | deploy | await | wire | done, required)
  - `updatedAt` (string (date-time), required)
  - `users` (PlatformUserView[] | null)
  - `variant` (enum: pocketid | authelia)
  - `warnings` (string[] | null, required)
  - `wiring` (PlatformWiringView)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /platform-services/{id}

Remove a platform service and unwire its integration

`confirm` must equal the kind. The integration is removed first; an integration still in use (a backup destination with schedules or stored backups, a tailnet in use, people who sign in only through the provider) answers 409 and nothing changes. `deleteData` also deletes the data volumes.

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

Parameters:
- `id` (path, string, required)
- `confirm` (query, string): Must equal the kind.
- `deleteData` (query, boolean): Also delete the data volumes.
- `force` (query, boolean): Single sign-on: remove it although people can sign in only through it.

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /platform-services/{id}/admin-link

Create a one-time sign-in link for the first administrator of Pocket ID (instance administrators)

Asks Pocket ID for a one-time access token for the administrator created at set-up and returns the link. The link works once and expires after an hour. It is shown once: it is never stored, logged or audited (the audit entry says who asked). Pocket ID has no passwords: the link signs the person in, and they register a passkey from their account page. Only for the Pocket ID variant of single sign-on.

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

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

Response 200: OK (AdminLink)
  - `email` (string, required)
  - `expiresAt` (string (date-time), required)
  - `url` (string, required): Open it once to sign in to Pocket ID and register a passkey. Shown once: it is not stored.
  - `username` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error; 502 Bad Gateway

## PUT /platform-services/{id}/backup

Schedule a daily volume backup of a platform service's data

Uses the volume backup feature (restic) with the chosen destination: daily at 03:17 UTC, 7 daily, 4 weekly and 3 monthly kept.

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

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

Request body (application/json, required): BackupInputBody
  - `destinationId` (string, required)

Response 200: OK (PlatformServiceView)
  - `backup` (PlatformBackupView)
  - `createdAt` (string (date-time), required)
  - `details` (map of string, required)
  - `environmentSlug` (string)
  - `hostname` (string, required)
  - `id` (string, required)
  - `kind` (enum: s3 | loki | grafana | headscale | idp | registry, required)
  - `label` (string, required)
  - `message` (string, required): What is happening or why it failed.
  - `projectId` (string)
  - `publicUrl` (string, required)
  - `serverId` (string)
  - `serverName` (string)
  - `services` (PlatformServiceRef[] | null, required)
  - `status` (enum: provisioning | ready | failed | removing, required)
  - `step` (enum: configure | deploy | await | wire | done, required)
  - `updatedAt` (string (date-time), required)
  - `users` (PlatformUserView[] | null)
  - `variant` (enum: pocketid | authelia)
  - `warnings` (string[] | null, required)
  - `wiring` (PlatformWiringView)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /platform-services/{id}/reconnect

Run the wiring of a ready platform service again

Headscale mints a new API key and updates the Tailscale integration; the others re-create a missing destination or drain and test it again.

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

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

Response 202: Accepted (PlatformServiceView)
  - `backup` (PlatformBackupView)
  - `createdAt` (string (date-time), required)
  - `details` (map of string, required)
  - `environmentSlug` (string)
  - `hostname` (string, required)
  - `id` (string, required)
  - `kind` (enum: s3 | loki | grafana | headscale | idp | registry, required)
  - `label` (string, required)
  - `message` (string, required): What is happening or why it failed.
  - `projectId` (string)
  - `publicUrl` (string, required)
  - `serverId` (string)
  - `serverName` (string)
  - `services` (PlatformServiceRef[] | null, required)
  - `status` (enum: provisioning | ready | failed | removing, required)
  - `step` (enum: configure | deploy | await | wire | done, required)
  - `updatedAt` (string (date-time), required)
  - `users` (PlatformUserView[] | null)
  - `variant` (enum: pocketid | authelia)
  - `warnings` (string[] | null, required)
  - `wiring` (PlatformWiringView)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /platform-services/{id}/retry

Retry a failed platform service

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

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

Response 202: Accepted (PlatformServiceView)
  - `backup` (PlatformBackupView)
  - `createdAt` (string (date-time), required)
  - `details` (map of string, required)
  - `environmentSlug` (string)
  - `hostname` (string, required)
  - `id` (string, required)
  - `kind` (enum: s3 | loki | grafana | headscale | idp | registry, required)
  - `label` (string, required)
  - `message` (string, required): What is happening or why it failed.
  - `projectId` (string)
  - `publicUrl` (string, required)
  - `serverId` (string)
  - `serverName` (string)
  - `services` (PlatformServiceRef[] | null, required)
  - `status` (enum: provisioning | ready | failed | removing, required)
  - `step` (enum: configure | deploy | await | wire | done, required)
  - `updatedAt` (string (date-time), required)
  - `users` (PlatformUserView[] | null)
  - `variant` (enum: pocketid | authelia)
  - `warnings` (string[] | null, required)
  - `wiring` (PlatformWiringView)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /platform-services/{id}/reveal

Reveal the generated credentials of a platform service (audited)

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

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

Response 200: OK (RevealOutputBody)
  - `items` (PlatformCredential[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /platform-services/{id}/users

People of the single sign-on provider

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

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

Response 200: OK (UsersOutputBody)
  - `items` (PlatformUserView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /platform-services/{id}/users

Add a person to the single sign-on provider (instance administrators)

Rewrites the users file in the service's configuration and deploys it. Without a password one is generated and returned once.

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

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

Request body (application/json, required): AddUserInputBody
  - `displayName` (string, required)
  - `email` (string, required)
  - `password` (string): Empty generates one (returned once).
  - `username` (string, required)

Response 201: Created (PlatformUserChange)
  - `applying` (boolean, required): The change is being deployed (the sign-in page restarts for a few seconds).
  - `generatedPassword` (string): Shown once.
  - `user` (PlatformUserView, required)
  - `warning` (string)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /platform-services/{id}/users/{username}

Remove a person from the single sign-on provider (instance administrators)

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

Parameters:
- `id` (path, string, required)
- `username` (path, string, required)

Response 200: OK (PlatformUserChange)
  - `applying` (boolean, required): The change is being deployed (the sign-in page restarts for a few seconds).
  - `generatedPassword` (string): Shown once.
  - `user` (PlatformUserView, required)
  - `warning` (string)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /platform-services/{id}/users/{username}/reset-password

Set a new password for a person (instance administrators)

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

Parameters:
- `id` (path, string, required)
- `username` (path, string, required)

Request body (application/json, required): ResetInputBody
  - `password` (string): Empty generates one (returned once).

Response 200: OK (PlatformUserChange)
  - `applying` (boolean, required): The change is being deployed (the sign-in page restarts for a few seconds).
  - `generatedPassword` (string): Shown once.
  - `user` (PlatformUserView, required)
  - `warning` (string)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error
