System API
Self-hosted instance administration — onboarding, SSH servers, component install, tunnels, filesystem browse, team-mode migration, and whole-instance data transfer.
The System API runs the machinery behind a self-hosted Alaf Server box: first-run onboarding, the SSH
servers you deploy onto, installing the components those servers need (Docker, OpenResty, certbot…),
port-forward tunnels, browsing the host filesystem, migrating a single-user instance into team mode, and
exporting or importing the entire instance. In the dashboard this is spread across the onboarding wizard and
the Settings → Servers area; from the terminal it's alaf server server and
alaf server system.
Self-hosted only
This module is mounted only when the API is not running in cloud mode — in cloud mode the routes and
their controllers are never loaded (node:fs, admin-user creation, and SSH management don't exist in the
cloud runtime). Every path below is unavailable on Alaf Server Cloud.
Base path & auth
All paths are relative to your instance, under /api — e.g. https://your-host/api/system/servers.
Send a personal access token as a bearer header (Authorization: Bearer <token>), created with
alaf server token create. The dashboard uses your session cookie instead. See the
API overview and auth model for details.
Three route groups are exceptions: /onboarding and /upgrade-to-auth are public (they run before any
user or session exists), and the two /setup routes are public to the user session but guarded by
internalAuth — a shared token used only by the desktop app pushing config to its embedded API.
Endpoints
| Method & path | Permission | What it does |
|---|---|---|
GET /api/system/onboarding | public | First-run check — is any server configured yet? |
POST /api/system/onboarding | public | First-run setup; persists settings and creates the first server. Returns 403 once configured. |
POST /api/system/setup | public · internalAuth | Push instance config from the desktop app. |
GET /api/system/setup | public · internalAuth | Read instance config for the desktop app. |
POST /api/system/upgrade-to-auth | public | Promote a zero-auth instance to email/password login. |
GET /api/system/settings | settings:read | Read instance settings. |
PATCH /api/system/settings | settings:write | Update instance-level (non-SSH) settings. |
DELETE /api/system/settings | settings:admin | Reset instance settings and remove all servers. |
GET /api/system/servers | server:list | List servers in the active organization. |
GET /api/system/servers/:id | server:read | Get one server (SSH secrets omitted). |
POST /api/system/servers | server:write | Add an SSH server. |
PATCH /api/system/servers/:id | server:write | Update an SSH server. |
DELETE /api/system/servers/:id | server:admin | Delete an SSH server. |
GET /api/system/servers/:id/rate-limit | server:read | Read the server's OpenResty rate-limit config. |
PATCH /api/system/servers/:id/rate-limit | server:write | Update the OpenResty rate-limit config (handler requires admin). |
GET /api/system/servers/:id/tunnels | server:read | List port-forward tunnels and live status (desktop only). |
POST /api/system/servers/:id/tunnels | server:write | Save a port-forward config (desktop only). |
POST /api/system/servers/:id/tunnels/:tunnelId/start | server:write | Open a tunnel. |
POST /api/system/servers/:id/tunnels/:tunnelId/stop | server:write | Close a tunnel. |
DELETE /api/system/servers/:id/tunnels/:tunnelId | server:write | Delete a tunnel config. |
POST /api/system/test-connection | server:write | Test SSH credentials without saving them. |
POST /api/system/check | server:write | Run component health checks against a saved server. |
POST /api/system/install | server:admin | Install one component on a server. |
POST /api/system/remove | server:admin | Remove one component from a server. |
POST /api/system/install/stream | server:admin | Install multiple components with live SSE logs. |
GET /api/system/install/stream | server:read | Re-attach to a running install session's SSE stream. |
GET /api/system/install/session | server:read | Get the active (or a specific) install session's state. |
GET /api/system/monitor/stream | server:read | Stream live CPU / memory / disk stats over SSE. |
GET /api/system/browse | settings:read | List child directories of a path (folder picker). |
POST /api/system/migration/preflight | settings:admin | Team-migration readiness check (read-only). |
POST /api/system/migration/start | settings:admin | Migrate single-user → your own remote server. |
POST /api/system/migration/start-cloud | settings:admin | Migrate → Alaf Server Cloud. |
POST /api/system/migration/start-tunnel | settings:admin | Expose this instance via an edge tunnel. |
POST /api/system/migration/switch-back | settings:admin | Reverse a migration back to single-user. |
POST /api/system/data-transfer/export | settings:admin | Export the whole instance (owner only). |
POST /api/system/data-transfer/import | settings:admin | Import a whole-instance file, ≤500 MB (owner only). |
Server routes are org-scoped and IDOR-safe
A :id that isn't a server in your active organization returns 404, indistinguishable from a missing
one. The server:* permission is resolved per-server on top of the route tag.
Onboarding & setup
GET /api/system/onboarding returns { configured: boolean } — true once at least one server exists. The
POST variant runs the same logic as the internal /setup push but only while the instance is unconfigured
(it returns 403 "Instance already configured" afterward), so first-run flows don't need a token.
POST /api/system/onboardingProp
Type
alaf server system onboarding apply --ssh-host 203.0.113.10 --ssh-user root --ssh-auth-method agentThe POST /api/system/setup route accepts the same body but is reserved for the desktop app and requires the
internalAuth shared token rather than a user session; it can also target an existing server by passing
serverId.
Instance settings
GET /api/system/settings returns the instance config (auth mode, tunnel provider, default build mode and
rollback window, invitation mail source, team mode, migration target). PATCH updates only the fields you
send. DELETE wipes instance settings and every server row.
PATCH /api/system/settingsProp
Type
curl -X PATCH https://your-host/api/system/settings \
-H "Authorization: Bearer $ALAFSERVER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"defaultBuildMode":"docker"}'# Same thing from the CLI:
alaf server system settings set --default-build-mode dockerZero-auth is double-gated
Setting authMode: "none" on anything other than a desktop deployment means anyone who can reach the API acts
as admin. The operator must both start the process with ALAFSERVER_ALLOW_ZERO_AUTH=true (else 403) and
send confirm: "I-understand-no-auth" in the body (else 400).
Promote zero-auth to a real login
POST /api/system/upgrade-to-auth turns the synthetic zero-auth user into an email/password account and flips
authMode from none to local, keeping the same user id so existing projects, deployments, and audit rows
still resolve. It is only callable while authMode === "none" (otherwise 400).
POST /api/system/upgrade-to-authProp
Type
alaf server system upgrade-to-auth --name "Ada" --email ada@example.comServers
A server is an SSH target you deploy onto. Responses never include SSH secrets — passwords and key passphrases are encrypted at rest and only decrypted inside the SSH client. See the custom servers guide.
POST /api/system/serversProp
Type
PATCH /api/system/servers/:id takes the same fields, all optional — only the keys you send are changed.
curl -X POST https://your-host/api/system/servers \
-H "Authorization: Bearer $ALAFSERVER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sshHost":"203.0.113.10","sshUser":"root","sshAuthMethod":"agent"}'# Same thing from the CLI:
alaf server server add --host 203.0.113.10 --user root --auth-method agentServer health check & install
Test a connection before saving, check what a saved server has installed, then install or remove components.
These routes identify the server by serverId in the body (not a URL param), and each handler runs its
own per-server server:admin (or server:read) check.
POST /api/system/test-connection opens an ephemeral SSH connection from body credentials and returns
{ ok, message } — nothing is persisted. It requires the caller be an org owner/admin.
POST /api/system/checkProp
Type
Returns { components, ready, missing }.
POST /api/system/installProp
Type
POST /api/system/remove mirrors install for a single removable component. POST /api/system/install/stream
takes { serverId, components: string[], config? } and returns a Server-Sent Events stream
(progress, log, complete, end); it returns 409 install_in_progress if a session is already
running.
# One-shot install:
alaf server server install <serverId> --component docker
# Stream logs live (attaches to POST /api/system/install/stream):
alaf server server install <serverId> --component docker --component openresty --followSSE, not JSON
install/stream and monitor/stream are event streams. Read GET /api/system/install/session to poll a
running session's state instead, or GET /api/system/install/stream?id=<sessionId> to re-attach after a page
reload.
GET /api/system/monitor/stream?serverId=<id> emits a stats event every few seconds with CPU %, memory,
disk, uptime, and load average gathered over SSH — alaf server server monitor <serverId>.
Per-server rate limiting
Read or update the OpenResty request rate limit for a server. OpenResty on the target server is the source of
truth; the API parses and rewrites its ratelimit.conf.
PATCH /api/system/servers/:id/rate-limitProp
Type
alaf server server rate-limit <serverId> --rps 50 --burst 100Port-forward tunnels
Desktop-only. Forward a remote server port to localhost on the operator's machine, VS Code style. Every
handler additionally asserts desktop mode; on a non-desktop instance they refuse.
POST /api/system/servers/:id/tunnelsProp
Type
Start / stop / delete a saved tunnel with POST .../tunnels/:tunnelId/start, .../stop, and
DELETE .../tunnels/:tunnelId. A started tunnel returns the actually-bound localPort and a
http://localhost:<port> URL.
Browse the host filesystem
GET /api/system/browse?path=<dir> lists child directories of a path (defaults to the host's home
directory), flagging which look like deployable projects. Used by the "deploy a local folder" picker.
alaf server system browse /srv/appsReturns { path, directories: [{ name, path, isProject }] } — projects first, then alphabetical. Non-directory
paths return 400, missing paths 404.
Team-mode migration
Move a single-user instance into team mode along one of three paths, or reverse it. All four routes require
org admin/owner and refuse (409) unless teamMode === "single_user". See the
cloud connect / migration guide.
Preflight and start (Path A — your own server) share a body:
POST /api/system/migration/startProp
Type
POST /api/system/migration/start-cloud— Path B, migrate to Alaf Server Cloud. Body:{ allowNonEmptyTarget?: boolean }.POST /api/system/migration/start-tunnel— Path C, expose via an edge tunnel. Body:{ slug: string }(required).POST /api/system/migration/switch-back— reverse any of them. Body:{ abandonRemote?: boolean }.
# Readiness check first, then migrate onto your server:
alaf server system migration preflight --server-id <id> --hostname app.example.com
alaf server system migration start --server-id <id> --hostname app.example.comSwitch-back cuts teammates off
Reversing to single-user mode revokes teammate access. The remote keeps a copy for a 30-day grace period so you can recover if you change your mind.
Whole-instance data transfer
Export or import the entire database — every organization's data. Both routes are owner-only
(requireRole("owner") on top of the settings:admin tag), because the tag alone would also admit admins and
members.
POST /api/system/data-transfer/exportProp
Type
POST /api/system/data-transfer/importProp
Type
alaf server system data-transfer export --passphrase "$SECRET" --out instance.json
alaf server system data-transfer import --file instance.json --passphrase "$SECRET" --mode wipeImport limits & errors
The import body is capped at 500 MB (413 PAYLOAD_TOO_LARGE above it). A wrong or missing passphrase
or a malformed file returns 400; primary-key collisions in merge mode return 409 PK_COLLISION (use
wipe, or clear the conflicting data first); a concurrent migration/import returns 503 BUSY.