Key points
- Only
/v1/setup/preflightis public. The rest of this surface stays reachable while the workspace is locked but still requires a token — the bootstrap administrator signs in first, then runs the wizard. - Normal authenticated routes stay locked until
POST /v1/setup/bootstrapsucceeds once. /v1/setup/preflightnames what is blocking, and a check may carrysuggested_envwith the variables that would resolve it.- Template routes render the GitOps seed without writing anything; the archive route returns the same files as a zip.
- Bootstrap works exactly once, and needs a token but no AAA resource check: there is nothing configured yet to check against.
Operations
GET/v1/setup/preflightPublic
Reports what still blocks setup, including a database that is still starting.
Notes
Public because a stuck install has no working login, so diagnosis cannot require one. GET /v1/setup/license is public for a different reason: terms nobody was shown are worth little.
Call it
curl -s "$NOPSAI_URL/v1/setup/preflight" | jqResponses
Preflight ran. Read ready rather than the status code when the platform is already serving.
{
"ready": true,
"can_login": true,
"mode": "ready",
"config_path": "/app/config.yml",
"env_file_path": "/app/.env",
"checks": [
{
"id": "database",
"label": "Database",
"status": "ok",
"message": "database is reachable",
"required": true
}
]
}Preflight-only mode, and the platform is not ready yet. The body is the same document.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 503 | A required check is failing, most often a database that has not finished starting. | Read checks[].message. A check may carry suggested_env, which names the variables that would resolve it. |
Side effects
- None. Preflight inspects configuration and connectivity; it changes nothing.
Proven by
services/nopsai/setup_wizard_test.goservices/nopsai/enterprise_gates_test.goservices/nopsai/setup_preflight.go
GET/v1/setup/statusAuthenticated
Whether first-install setup has already completed.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/setup/status" | jq '{completed, profile, counts}'Responses
Setup state, what was seeded, and the checks the wizard evaluates.
{
"completed": true,
"completed_at": "2026-08-19T10:12:44Z",
"profile": "local",
"runtime_env": "local",
"counts": {},
"checks": [],
"starter_profiles": []
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | Setup status could not be built, which usually means the database is unreachable. | Check /v1/setup/preflight first — it names the failing dependency. |
Side effects
- None.
Proven by
services/nopsai/setup_wizard_test.goservices/nopsai/setup_wizard_handlers.goservices/nopsai/setup_wizard.go
GET/v1/setup/templatesAuthenticated
GitOps seed templates offered by the wizard.
Notes
There is no route-specific failure: an unrecognised profile is normalised to a known one rather than rejected, so check the profile in the response rather than expecting a 400.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
profile | query | string | Optional | Which starter profile to render templates for. An unknown value is normalised rather than rejected. |
repositories | query | string | Optional | Repositories to seed configuration for, shaping the generated team and trigger documents.Repeatable |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/setup/templates?profile=local" | jq '.files[].path'Responses
The profile that was resolved and the files it renders.
{
"profile": "local",
"files": [
{ "path": "setting/system/auth.yaml", "content": "..." }
]
}Side effects
- None. Rendering a template writes nothing.
Proven by
services/nopsai/setup_wizard_test.goservices/nopsai/setup_wizard_handlers.goservices/nopsai/setup_wizard_templates.go
GET/v1/setup/templates.zipAuthenticated
Same templates as a downloadable archive.
Notes
Same normalisation as the JSON route: no route-specific failure, and an unknown profile yields the default rather than an error.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
profile | query | string | Optional | Starter profile to render, as for the JSON route. |
repositories | query | string | Optional | Repositories to seed configuration for.Repeatable |
Call it
curl -s -o nopsai-templates.zip -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/setup/templates.zip?profile=local"
unzip -l nopsai-templates.zipResponses
Zip archive of the rendered templates.
Side effects
- None.
Proven by
services/nopsai/setup_wizard_test.goservices/nopsai/setup_wizard_handlers.go
POST/v1/setup/bootstrapAuthenticated
Runs the one-time bootstrap. Normal authenticated routes stay locked until this succeeds once.
Notes
It works exactly once. It needs a token — the bootstrap administrator’s — but no AAA resource check, because there is nothing configured yet to check against.
Call it
curl -sX POST "$NOPSAI_URL/v1/setup/bootstrap" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @bootstrap.json | jqResponses
Bootstrap applied. The response is the same document /v1/setup/status returns.
{
"completed": true,
"completed_at": "2026-08-19T10:12:44Z",
"profile": "local",
"runtime_env": "local",
"env_file_path": "/app/.env",
"counts": {},
"checks": [],
"starter_profiles": []
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The payload is malformed, or an answer is not valid for the chosen profile. | The message names the field. Apply errors deliberately include the actionable write or configuration reason. |
| 500 | Local secret generation or a configuration write failed. | Read the message: it names the path or setting that could not be written, which is usually a permissions problem in the mounted config volume. |
Side effects
- Creates the first administrator and marks setup complete.
- May generate local secrets and write them to the env file the response names.
- May seed the GitOps layout and repository teams for the chosen profile.
Proven by
services/nopsai/setup_wizard_test.goservices/nopsai/setup_wizard_handlers.goservices/nopsai/bootstrap_schema.go
GET/v1/setup/licensePublic
The licence notice, its version and digest, and whether this installation has accepted it.
Notes
Public on purpose. Container images are freely pullable and already carry this exact text, and an administrator has to be able to read terms before accepting them.
Call it
curl -s "$NOPSAI_URL/v1/setup/license" | jq '{document_version, document_sha256, accepted, accepted_at}'Responses
The notice text, its identity, and the current acceptance record.
{
"text": "NopsAI Licence\n...",
"document_version": "2026-02",
"document_sha256": "13173227932dbde8...",
"accepted": false
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The acceptance state could not be read from the database. | Check database connectivity. Acceptance is never assumed when it cannot be evaluated. |
Side effects
- None. Reading the notice records nothing.
Proven by
services/nopsai/setup_license_test.gocontract/license_notice_test.goservices/nopsai/setup_license.gopkg/licensenotice/licensenotice.go
POST/v1/setup/license/acceptAuthenticated
Record an administrator’s acceptance of the licence notice, which setup completion requires.
Notes
Until this is recorded, POST /v1/setup/bootstrap answers 412 and the first-install gate keeps the rest of the API locked, so an installation that never accepts never becomes usable.
Call it
curl -s -X POST -H "Authorization: Bearer $NOPSAI_TOKEN" -H "Content-Type: application/json" -d '{"accept": true, "document_sha256": "$DIGEST"}' "$NOPSAI_URL/v1/setup/license/accept"Responses
Acceptance recorded against the notice version and digest.
{
"accepted": true,
"accepted_at": "2026-08-23T10:00:00Z",
"accepted_by": "admin",
"document_version": "2026-01"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The request did not set accept: true. | Acceptance must be explicit; there is no implicit path. |
| 409 | The supplied digest does not match the notice the server is serving. | Re-read GET /v1/setup/license and accept the current wording. A browser tab open across an upgrade will hit this. |
| 403 | The caller could not be identified. | Acceptance records who agreed, so an unidentified caller cannot accept. |
Side effects
- Writes
license_accepted_at,license_accepted_by,license_document_version, andlicense_document_sha256tosetup_state. - Writes a
system.license.acceptaudit entry.
Proven by
services/nopsai/setup_license_test.goservices/nopsai/setup_license.goservices/nopsai/setup_wizard_status.go
How it works
Preflight and status answer different questions, and their access classes follow. Preflight asks "can this install be set up right now?" and is public, because a cold start has no working login. Status asks "has it already been set up?" and needs a token, because by then there is one to have.
Bootstrap errors are deliberately specific: an apply failure names the write or configuration reason, which is almost always a permissions problem on the mounted configuration volume rather than a bad payload.
Implementation evidence
services/nopsai/setup_preflight.goPreflight checks and the preflight-only server.
services/nopsai/setup_wizard_handlers.goStatus, templates, and bootstrap handlers.

