Admin & lifecycle¶
The control-plane routes — the ones that must stay reachable even when data writes are
frozen. They switch the daemon between read-only and read-write, re-run adoption
(/import), report and drive repo sync, poll background jobs, expose the live event
stream, and finally stop the daemon. /admin/mode, /import, and /sync deliberately
carry no write gate so a not-yet-adopted or read-only repo stays recoverable.
The fixture is a plain directory, not a git clone, so sync is disabled on it — the
examples below prove exactly that state. Each example runs against a live daemon — see
how the reference is tested. Reads and writes here need the matching admin
capability (admin.mode, admin.migrate, sync.read, sync.run, admin.shutdown).
Routes¶
| Method | Path | Cmdlet | Cap | Write |
|---|---|---|---|---|
| GET | /events |
— (SSE stream) | events.subscribe |
— |
| GET | /jobs/{id} |
— | entity.read |
— |
| GET | /jobs/{id}/result |
— | entity.read |
— |
| POST | /admin/mode |
Set-NerthusMode |
admin.mode |
— |
| POST | /admin/shutdown |
Stop-Nerthus |
admin.shutdown |
— |
| GET | /sync |
Get-NerthusSync |
sync.read |
— |
| POST | /sync |
Invoke-NerthusSync |
sync.run |
— |
| POST | /import |
Initialize-NerthusRepo |
admin.migrate |
— |
Cap is the required capability (— = public, no token); Write (✓) marks routes that pass the write gate; a — in Cmdlet is reached directly, no wrapper. Paths are relative to /v1/api; the cross-cutting contract — middleware, envelopes, status codes — is on the API reference index.
- These are control-plane routes, deliberately write-gate exempt so they stay reachable to recover from a lock.
POST /importclears aSchemaTooOld403 on an un-adopted repo;?dryRun=truepreviews the index without writing (Adoption). - No role bundle carries
sync.read/sync.run— both resolve throughadmin.allor a per-Person grant; the scheduled tick runs inside the daemon and needs no token. The sync subsystem: Sync.
Freeze writes (read-only)¶
POST /v1/api/admin/mode with { "mode": "read-only" } puts the daemon in ReadOnly;
every write-gated route then answers 403 { "error": "SchemaTooOld" }. The route itself
is never gated, so it always works.
Response 200:
Resume writes (read-write)¶
{ "mode": "read-write" } returns the daemon to ReadWrite so data routes accept
writes again.
Response 200:
Re-run adoption¶
POST /v1/api/import runs the idempotent importer: it rebuilds the single index, files
free roster notes into the character files, and stamps the format version. Applied is
true on a real run (?dryRun=true previews without writing). The counts describe the
emitted index.
Response 200 (trimmed):
{ "Applied": true,
"Entities": { "players": 2, "characters": 2, "npc": 3, "locations": 7, "maps": 12, "preserved": true },
"SessionsObserved": 1 }
Sync status¶
GET /v1/api/sync reports which commit the daemon serves and its scheduler state. On the
fixture — a plain directory, not a git repo — sync is off: enabled, git, and stale
are all false.
Response 200 (trimmed):
Trigger a sync (disabled)¶
POST /v1/api/sync runs one fetch/converge tick — but only where sync is enabled
host-locally. On a daemon with sync off it refuses outright with 409
{ "error": "SyncDisabled" } rather than publish an uncommitted worktree.
Response 409:
{ "error": "SyncDisabled", "detail": "sync is not enabled on this daemon — enable it host-locally (.nerthus/local/sync.json or -SyncIntervalMinutes) and restart" }
Poll a background job¶
GET /v1/api/jobs/{id} returns the record for a background job (the 202 envelope some
workflows emit). An unknown id is 404 { "error": "job not found" }.
Response 404:
Fetch a job's result¶
GET /v1/api/jobs/{id}/result returns { jobId, status, result } for a finished job;
an unknown id is likewise 404.
Response 404:
Subscribe to events (anonymous)¶
GET /v1/api/events is the live SSE stream — connect with Accept: text/event-stream
and a token carrying events.subscribe. Without any credentials it is 401
{ "error": "Unauthorized" }.
Response 401:
Subscribe to events (missing capability)¶
A valid token that lacks events.subscribe — here one minted with only entity.read —
is rejected with 403 { "error": "Forbidden" } before any stream opens.
Response 403:
Stop the daemon¶
POST /v1/api/admin/shutdown returns 200 { "stopped": true } and then stops the
process — it is always the last call you make against a daemon.
Response 200: