# Config API

10 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 /environments/{environmentId}/config-source

Repository binding and status of an environment's nebula.toml

How the environment finds its file (`explicit`, `derived` from the services' repositories, or `none`), whether auto-apply is on, the status of the last check (`in_sync`, `differs`, `invalid`, `not_found`, `unreachable`, `error`) and the state of each service.

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

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

Response 200: OK (EnvironmentConfigSource)
  - `autoApply` (boolean, required)
  - `branch` (string, required)
  - `checkedAt` (string (date-time))
  - `commit` (string, required): Commit the file was read at (empty when it was read at the branch head through an API)
  - `connectionId` (string)
  - `fileSha256` (string, required)
  - `mode` (enum: explicit | derived | none, required): explicit: set on the environment; derived: the environment's git services share one repository and branch; none: nothing is read
  - `path` (string, required): Explicit path; empty when the default rule applies
  - `repo` (string, required)
  - `resolvedPath` (string, required): Path the file is read from (or was last read from)
  - `services` (EnvironmentConfigService[] | null, required)
  - `status` (enum: unknown | in_sync | differs | invalid | not_found | unreachable | error, required)
  - `statusDetail` (string, required): Why the status is what it is: the first error of the file, why it could not be read, or why auto-apply refused
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PUT /environments/{environmentId}/config-source

Bind an environment to a nebula.toml in a repository

Sets the repository, branch and path the environment's file is read from, and whether a push stages it (`autoApply`). `connectionId`, `repo` and `branch` are set together or not at all. Auto-apply needs the admin scope, is refused for principals that need production approval (403) and for production environments (422 `production_auto_apply_unsupported`); it stages, it never deploys.

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

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

Request body (application/json, required): ConfigUpdateSourceInput
  - `autoApply` (boolean): Stage the file on every push to the branch. Needs the admin scope, a person (not an agent) and a non-production environment. Never deploys.
  - `branch` (string): Branch the file is read from
  - `connectionId` (string): Git connection to read the file through; give it together with repo and branch
  - `path` (string): Path of the file in the repository; empty applies the default rule (nebula.toml at the root, else in the build context)
  - `repo` (string): Repository (owner/name) on the connection

Response 200: OK (EnvironmentConfigSource)
  - `autoApply` (boolean, required)
  - `branch` (string, required)
  - `checkedAt` (string (date-time))
  - `commit` (string, required): Commit the file was read at (empty when it was read at the branch head through an API)
  - `connectionId` (string)
  - `fileSha256` (string, required)
  - `mode` (enum: explicit | derived | none, required): explicit: set on the environment; derived: the environment's git services share one repository and branch; none: nothing is read
  - `path` (string, required): Explicit path; empty when the default rule applies
  - `repo` (string, required)
  - `resolvedPath` (string, required): Path the file is read from (or was last read from)
  - `services` (EnvironmentConfigService[] | null, required)
  - `status` (enum: unknown | in_sync | differs | invalid | not_found | unreachable | error, required)
  - `statusDetail` (string, required): Why the status is what it is: the first error of the file, why it could not be read, or why auto-apply refused
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity (`production_auto_apply_unsupported`); 500 Internal Server Error

## DELETE /environments/{environmentId}/config-source

Clear the explicit binding of an environment

Removes the explicit repository binding and switches auto-apply off. Which fields services own stays as it is.

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

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

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

## POST /environments/{environmentId}/config-source/check

Read the bound nebula.toml now

Reads the file at the branch head (or at `sha`), validates it, compares it with the environment and stores the status. Nothing is applied.

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

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

Request body (application/json): CheckConfigSourceRequest
  - `sha` (string): Commit to read; empty reads the head of the bound branch

Response 200: OK (EnvironmentConfigSource)
  - `autoApply` (boolean, required)
  - `branch` (string, required)
  - `checkedAt` (string (date-time))
  - `commit` (string, required): Commit the file was read at (empty when it was read at the branch head through an API)
  - `connectionId` (string)
  - `fileSha256` (string, required)
  - `mode` (enum: explicit | derived | none, required): explicit: set on the environment; derived: the environment's git services share one repository and branch; none: nothing is read
  - `path` (string, required): Explicit path; empty when the default rule applies
  - `repo` (string, required)
  - `resolvedPath` (string, required): Path the file is read from (or was last read from)
  - `services` (EnvironmentConfigService[] | null, required)
  - `status` (enum: unknown | in_sync | differs | invalid | not_found | unreachable | error, required)
  - `statusDetail` (string, required): Why the status is what it is: the first error of the file, why it could not be read, or why auto-apply refused
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /projects/{projectId}/config/apply

Apply a nebula.toml to an environment

Re-plans inside one transaction and stages the file exactly like dashboard edits (generation bumps, discardable, deployed with the next deploy), then runs the immediate-kind settings (scale, uptime, tasks, backups, git link, DNS) one by one. It never deploys unless `deploy: true`. Without `planId` it plans and applies atomically; with it, a plan that changed since answers 409 `plan_stale` (fresh plan in `details.plan`) and writes nothing. A plan with errors answers 422 `config_blocked` (plan in `details.plan`); missing scopes answer 403 `insufficient_scope` before any write. Principals that need production approval get `approval_required` for a plan with immediate changes in production. Status 201: `staged` or `partial` (read `steps`); 200: `noop` or `failed`. Running an apply again converges.

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.
- `projectId` (path, string, required)

Request body (application/json, required): ApplyConfigRequest
  - `deploy` (boolean): Deploy the services this apply touched that now have staged changes (needs deployments:write; not available to principals that need production approval)
  - `environment` (string): Environment slug (or id); may be omitted when the project has exactly one standard environment
  - `only` (string[] | null): Restrict the apply to these file key paths (prefixes)
  - `planId` (string): Plan id from a plan call: the apply fails with 409 plan_stale (and the fresh plan in details.plan) when anything changed since
  - `prune` (boolean): Also delete sub-resources the file owned before and no longer lists
  - `source` (ConfigPlanFileSource, required)

Response 201: Created (ConfigApplyResult)
  - `deployments` (string[] | null, required)
  - `plan` (ConfigPlan, required)
  - `planId` (string, required)
  - `services` (ConfigAppliedService[] | null, required)
  - `status` (enum: staged | partial | failed | noop, required)
  - `steps` (ConfigApplyStep[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden (`insufficient_scope`); 404 Not Found; 409 Conflict (`plan_stale`); 422 Unprocessable Entity (`config_blocked`); 500 Internal Server Error

## GET /projects/{projectId}/config/export

Export a project as a nebula.toml

Writes the live state of one environment, or of all standard environments (`environment=all`, factored into a base plus overrides), as a nebula.toml. Keys equal to their default are left out unless `defaults=true`. Secret values are never written: secrets become `secrets.keys` entries. The output is deterministic (same state, same bytes). Names that are not unique make the export fail with `ambiguous_reference`.

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

Parameters:
- `projectId` (path, string, required)
- `environment` (query, string): Environment slug, or "all" for every standard environment factored into a base plus [environments.<slug>] overrides; may be omitted when the project has exactly one standard environment
- `base` (query, string): With environment=all: the environment whose values form the base (default: production, else the first)
- `defaults` (query, boolean): Also write keys whose value equals the default

Response 200: The project as a nebula.toml: plain text by default, or `{ file, warnings }` JSON with `Accept: application/json`. A clean export plans to zero changes against the state it was generated from. (ConfigExportResult)
  - `file` (string, required): The nebula.toml text
  - `warnings` (ConfigDiagnostic[] | null, required): Things the file cannot express and findings the first apply would report
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /projects/{projectId}/config/plan

Plan a nebula.toml against an environment

Computes what applying the file would do to one environment: services to create, update or leave alone, every changed field labelled `staged` (takes effect on the next deploy) or `immediate` (takes effect at once, like the dashboard control), drift against the dashboard, services that are not in the file, and the scopes an apply needs. Secret and generated values are masked. The answer is always 200: check `valid` and the diagnostics. `planId` identifies the plan; pass it to apply to refuse an apply whose inputs changed meanwhile. The file is inline (`source.file`) or read through the environment's repository binding (`source.git`, needs projects:write). Nothing is written.

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

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

Request body (application/json, required): PlanConfigRequest
  - `environment` (string): Environment slug (or id); may be omitted when the project has exactly one standard environment
  - `only` (string[] | null): Restrict the plan to these file key paths (prefixes), e.g. services.web
  - `prune` (boolean): Also delete sub-resources the file owned before and no longer lists (variables, domains, volumes, networks, links, tasks, backups, uptime). Never deletes services or databases
  - `source` (ConfigPlanFileSource, required)

Response 200: OK (ConfigPlan)
  - `diagnostics` (ConfigDiagnostic[] | null, required)
  - `environment` (ConfigPlanEnvironment, required)
  - `guard` (ConfigGuard, required)
  - `planId` (string, required)
  - `project` (ConfigPlanProject, required)
  - `requiredScopes` (string[] | null, required)
  - `schema` (integer (int64), required)
  - `services` (ConfigPlanService[] | null, required)
  - `shared` (ConfigSharedPlan, required)
  - `source` (ConfigPlanSource, required)
  - `summary` (ConfigPlanSummary, required)
  - `valid` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /projects/{projectId}/config/validate

Validate a nebula.toml

Parses the project file strictly (unknown keys, wrong types, limits), resolves it, and checks it statically: kinds, references, bounds, secrets, paths. With `environment` it also resolves that environment's view and the names the file uses (servers, git connections, destinations) against the project. Invalid input is data, not an HTTP error: the answer is 200 with `valid: false` and diagnostics (code, file line and column, "did you mean" suggestions). Nothing is written.

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

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

Request body (application/json, required): ValidateConfigRequest
  - `attachments` (map of string): Files the file refers to (compose_file, files.*.source), keyed by their path relative to the file
  - `environment` (string): Also validate this environment's resolved view and resolve names against the project (slug)
  - `file` (string, required): The nebula.toml text

Response 200: OK (ConfigValidateResult)
  - `diagnostics` (ConfigDiagnostic[] | null, required)
  - `environments` (string[] | null, required): Environment blocks of the file
  - `schema` (integer (int64), required)
  - `services` (ConfigValidateService[] | null, required)
  - `valid` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /services/{serviceId}/config-source

Whether a service is managed by a nebula.toml

Which file last applied to the service, when and by whom, and the drift state of every field the file owns (`in_sync`, `file_changed`, `drift`, `conflict`). Computed from stored fingerprints, without reading the file.

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

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

Response 200: OK (ServiceConfigSource)
  - `appliedAt` (string (date-time))
  - `appliedBy` (string)
  - `branch` (string)
  - `commit` (string)
  - `fields` (ConfigFieldState[] | null, required)
  - `fileSha256` (string)
  - `managed` (boolean, required)
  - `origin` (enum: git | cli | api | mcp | ui)
  - `path` (string)
  - `pending` (string[] | null, required): Immediate changes that could not run yet (the service is not deployed)
  - `repo` (string)
  - `state` (enum: in_sync | file_changed | drift | conflict)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /services/{serviceId}/config-source/release

Stop managing fields of a service with the file

Releases the named fields: the file no longer owns them and later applies leave their live value alone. Changes nothing else.

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

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

Request body (application/json, required): ReleaseConfigFieldsRequest
  - `paths` (string[] | null, required): Field ids to stop managing (the ids of fields in the service's config source)

Response 200: OK (ServiceConfigSource)
  - `appliedAt` (string (date-time))
  - `appliedBy` (string)
  - `branch` (string)
  - `commit` (string)
  - `fields` (ConfigFieldState[] | null, required)
  - `fileSha256` (string)
  - `managed` (boolean, required)
  - `origin` (enum: git | cli | api | mcp | ui)
  - `path` (string)
  - `pending` (string[] | null, required): Immediate changes that could not run yet (the service is not deployed)
  - `repo` (string)
  - `state` (enum: in_sync | file_changed | drift | conflict)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error
