Server API
How the nikcli server exposes CLI, TUI, mobile, and workspace routes through a single typed Effect HttpApi surface.
Overview
The server in packages/nikcli/src/server/server.ts is the shared HTTP surface for
web clients, the desktop app, the TUI control channel, mobile pairing, PTYs, workspace routing,
connector settings, MCP, analytics, and the generated SDK.
It runs on Bun.serve with a hand-written request pipeline in
server/server-router.ts. Every declared route lives in a typed
effect/unstable/httpapi group under
packages/nikcli/src/server/httpapi/, assembled into one API in
httpapi/public.ts and served through HttpApiBridge.
Earlier releases mounted classic Hono route files under
src/server/routes/ and put the typed groups behind a
NIKCLI_EXPERIMENTAL_HTTPAPI flag. Both are gone: the routes directory was
deleted, the flag was removed, and the typed groups are the only implementation. If you are
looking for routes/session.ts, read
httpapi/session.ts instead.
Typed HttpApi groups
One HttpApiGroup per domain — session, file, provider, mcp, mobile, sync,
loop, mission, analytics — with schema-checked request and response bodies.
Raw handlers
Streaming and upgrade paths that a schema cannot express (SSE feeds, prompt streaming,
webhooks, websocket upgrades) are served as plain Request → Response
handlers ahead of the bridge.
Instance-aware execution
Every non-global request gets an instance context from x-nikcli-directory,
directory, x-nikcli-workspace, or the session named in the path.
Layered auth
Mobile bearer tokens, account tokens, Tailscale identity, and Basic Auth all resolve to a
single Auth.Principal before dispatch.
Start a server
Use nikcli serve for the general API server, nikcli web for the
web app workflow, or nikcli mobile serve for the mobile-first host. The TUI also
starts an internal worker server when network options are provided.
nikcli serve --hostname 127.0.0.1 --port 4096
NIKCLI_SERVER_PASSWORD=secret \
nikcli serve --hostname 0.0.0.0 --port 4096
NIKCLI_SERVER_PASSWORD=secret \
nikcli mobile serve --hostname 0.0.0.0 --port 4096 --pair | Mode | Command | Primary routes | Notes |
|---|---|---|---|
| Local API | nikcli serve | All mounted server routes | Safe for loopback development. Password is optional on loopback. |
| Public API | nikcli serve --hostname 0.0.0.0 | All mounted server routes | Set NIKCLI_SERVER_PASSWORD or use loopback Tailscale identity mode. |
| Mobile host | nikcli mobile serve --pair | /mobile/*, /user/*, shared project/session routes | Bearer token auth is minted by the mobile pairing commands. |
| Workspace host | nikcli workspace-serve | /experimental/workspace/* plus the standard API | Hosts workspace contexts, status, events, and session proxying. |
| TUI external | nikcli --port 0 | TUI worker server plus standard API | Used when the terminal UI needs network-visible access. |
Server.listen() tries the requested port, then falls back to an ephemeral one
rather than failing. --port 0 tries 4096 first. The concrete URL is
always read back from the returned server, never assumed.
Request pipeline
ServerRouter.make() builds the fetch handler. Each request walks the same path:
- Body limit. Oversized bodies are rejected before anything is parsed (
middleware/body-limit.ts). - CORS preflight.
OPTIONSshort-circuits with the computed CORS headers. - Public share routes.
/s/:shareID,/share/:shareID, and/api/share/:shareIDanswer before any auth check. - Auth.
Auth.authenticate()resolves a principal from mobile tokens, account tokens, Tailscale identity, or Basic Auth. Public paths are allowed through unauthenticated. - Global vs instance split.
isInstanceLessPath()routes/global,/user,/account(plus the/sync/streamSSE feed) to the global branch, which never binds a project directory. - Context resolution. Everything else resolves a directory and workspace, proxies remote workspaces through
ServerProxy, and entersWorkspaceContext.provide+withInstanceAsync. - Websocket upgrades. PTY connect paths upgrade in place instead of returning a body.
- Raw handlers.
PublicRoutes.instanceRequest()claims SSE, prompt streaming, chatbot webhooks, and theextra.tsroutes. - HttpApi bridge. If
HttpApiBridge.supports(path, method)matches, the typed handler answers. - Fallback. Unmatched non-
/mobilepaths are proxied toapp.nikcli-ai.devso the hosted web app can be served from the same origin.
The raw handlers run before the bridge on purpose. A schema-encoded response cannot hold an open SSE stream or a websocket, so those paths have to claim the request first. Everything a schema can describe belongs in an HttpApi group.
Auth layers
Server auth is layered so browser user sessions, mobile device tokens, Tailscale identity, and
Basic Auth can coexist. All of it resolves in server/httpapi/auth.ts, which
returns a single principal the rest of the pipeline reads via
ServerRouter.principal().
| Layer | Accepted input | Behavior |
|---|---|---|
| Public paths | /user/login, /user/register, /user/status, share routes | Public by design so accounts can be created and login status can be checked. |
| Account token | Authorization: Bearer nku_… | Resolves the local account record; /account/* and /user/* run instance-less. |
| Mobile token | Authorization: Bearer or ?token= | Verified against hashed tokens in the mobile auth table; required for /mobile/* when mobileAuthRequired is set. |
| Tailscale identity | Tailscale-User-Login | Trusted only on a loopback listener with NIKCLI_SERVER_TAILSCALE_AUTH; allows users from NIKCLI_SERVER_TAILSCALE_USERS. |
| Basic Auth | Username and password | Username defaults to nikcli; password comes from NIKCLI_SERVER_PASSWORD. |
NIKCLI_SERVER_USERNAME=nikcli \
NIKCLI_SERVER_PASSWORD=secret \
nikcli serve --hostname 0.0.0.0 --port 4096
NIKCLI_SERVER_TAILSCALE_AUTH=1 \
NIKCLI_SERVER_TAILSCALE_USERS=alice@example.com,bob@example.com \
nikcli serve --hostname 127.0.0.1 --port 4096
The server logs a warning and refuses to trust identity headers when
NIKCLI_SERVER_TAILSCALE_AUTH is set but the listener is not bound to loopback —
otherwise anyone able to reach the port could forge the header.
Instance & workspace context
After auth, instance-bound routes run inside a project instance. The server chooses the
directory from the directory query parameter, the x-nikcli-directory
header, the current process cwd, or the directory stored on a session referenced by the route
path.
Workspace routing then looks at workspace, x-nikcli-workspace, or
the route session's workspaceID. Local workspaces replace the directory with the
workspace target directory. Remote workspaces are proxied through ServerProxy for
both HTTP and websocket upgrades.
| Input | Priority | Effect |
|---|---|---|
x-nikcli-workspace / ?workspace= | Highest | Pins the workspace and skips the session lookup entirely. |
directory query | High | Sets the instance directory unless a workspace target overrides it. |
x-nikcli-directory | High | Same as query; used by the SDK and mobile clients. |
| Session route id | Fallback | For /session/:id… paths the server recovers the session's directory and workspace. |
| Instance-less roots | Bypass | /global, /user, /account never bind a directory. |
httpapi/instance-less.ts owns the list. Four call sites — the bridge, the
server fallback, the router's global test, and PublicRoutes.globalRequest —
read it, so adding a global root is one edit instead of four silent 404s.
Route groups
Every group is declared in its own file under server/httpapi/ and composed into
PublicHttpApi.Api in httpapi/public.ts.
| Prefix | Source | Purpose |
|---|---|---|
/session | httpapi/session.ts | Session lifecycle, messages, parts, revert, share, todos, diffs, context breakdown, background delegations, monitors, v2 entries. |
/project | httpapi/project.ts | Project list, current project, and project updates. |
/config | httpapi/config.ts | Read and update config plus the provider summary. |
/provider | httpapi/provider.ts | Provider catalog, auth methods, API token write, auth removal. |
/mcp | httpapi/mcp.ts | MCP status, add, OAuth start/callback/authenticate, connect, disconnect, toggle. |
/permission | httpapi/permission.ts | Pending permission requests and replies. |
/question | httpapi/question.ts | User question requests, replies, and rejections. |
/pty | httpapi/pty.ts | PTY lifecycle: create, list, input, resize, close. The websocket connect path upgrades in the router. |
/experimental | httpapi/experimental.ts | Tool metadata and ids, worktrees, and other unstable surfaces. |
/experimental/workspace | httpapi/workspace.ts | Adaptors, create/list/remove/restore, status, session restore, warp. |
/mobile | httpapi/mobile.ts, handlers in server/mobile/ | Mobile bootstrap, sessions, repos, git, GitHub, approvals, routines, loops, missions, terminals, teleport, memory. |
/connectors | httpapi/connectors.ts | Connector status, auth, logout, cache invalidation. |
/chatbot | httpapi/chatbot-api.ts | Bot config CRUD, start/stop, and delivery state. |
/discord | httpapi/discord.ts | Discord bot setup and lifecycle. |
/brain | httpapi/brain.ts | Brain model selection, scheduler state, and manual triggers. |
/mission | httpapi/mission.ts | Mission list, templates, generate, validation review, and run endpoints. |
/loop | httpapi/loop.ts | Loops CRUD, templates, generate, run, pause, resume, schedule. |
/sync | httpapi/sync.ts | Event push, outbox, snapshots, stats, config, connect/disconnect/drain. See Sync. |
/analytics | httpapi/analytics.ts | Global, daily, and per-session analytics summaries and drilldowns. |
/doctor | httpapi/doctor.ts | Health, connectivity, and runtime diagnostics behind nikcli doctor. |
/profile | httpapi/profile.ts | Declared user profile and learned habits. |
/voice | httpapi/voice.ts | Audio transcription for voice input. |
/tui | httpapi/tui.ts | Remote control: append/submit/clear prompt, open dialogs, execute commands, toasts, control channel. |
/global | httpapi/global.ts | Instance-less health and dispose. /global/event is the raw SSE feed. |
/user | httpapi/users.ts | Local account login, registration, profile, and user admin. Instance-less. |
/account | httpapi/account.ts | nikcli account records and tokens. Instance-less. |
| (root) | httpapi/top-level.ts | /path, /vcs, /vcs/status, /vcs/diff/raw, /vcs/apply, /command, /agent, /skill, /lsp, /formatter, /instance/dispose. |
| (root) | httpapi/file.ts | /find, /find/file, /find/symbol, /file, /file/content, /file/status. |
| (root) | httpapi/app.ts | POST /log for client log forwarding and POST /skill for skill creation. |
Raw handlers
A handful of paths are served as plain Request → Response functions because their
shape cannot round-trip through a schema. Their contracts still live in the API — as
ContractExtraHttpApi groups on PublicApi — so the generated SDK and
the OpenAPI document stay complete.
| Path | Handler | Why it is raw |
|---|---|---|
GET /event | httpapi/event.ts | Long-lived instance SSE feed. |
GET /global/event | httpapi/event.ts | Long-lived instance-less SSE feed. |
GET /sync/stream | httpapi/sync.ts | Sync SSE feed; the only /sync/ path served instance-less. |
POST /session/:id/message | httpapi/prompt.ts | Streams assistant output as it is produced. |
POST /session/:id/prompt_async | httpapi/prompt.ts | Fire-and-forget prompt submission. |
/chatbot/* webhooks | httpapi/chatbot.ts | Provider-defined bodies and signature verification; falls through so /chatbot/bots* stays typed. |
/auth/:providerID, /config/reload, /config/mcp, /config/profiles | server/extra.ts | Union payloads and mutations the encoded router cannot express yet. |
/pty/:ptyID/connect | server/websocket.ts | Websocket upgrade. |
GET /companion | server/companion.ts | Serves the embedded companion HTML shell. |
/share/:shareID, /s/:shareID | server/public.ts | Must answer before auth. |
Session API
httpapi/session.ts is the largest group and the core of the SDK contract.
Streaming the prompt itself is the one carve-out — that lives in httpapi/prompt.ts.
| Method | Path | Behavior |
|---|---|---|
| GET | /session | List sessions, filtered by directory, roots, start timestamp, search, and limit. |
| POST | /session | Create a session with optional parent, title, permission rules, skills, GitHub metadata, and workspace id. |
| GET | /session/status | Return the current session status map. |
| GET/PATCH | /session/:sessionID | Read or update title and archive time. |
| POST | /session/:sessionID/fork | Fork from a session, optionally at a message id. |
| POST | /session/:sessionID/abort | Cancel prompt execution, owned delegation jobs, and monitors. |
| POST | /session/:sessionID/revert / /unrevert | Revert the diff associated with a message or part, or undo the previous revert. |
| POST | /session/:sessionID/summarize | Compact the transcript into a summary. |
| POST | /session/:sessionID/command, /shell | Run a prompt command or a shell command inside the session. |
| GET | /session/:sessionID/message | List messages, optionally limited. |
| GET/PATCH | /session/:sessionID/message/:messageID[/part/:partID] | Read a message or patch an individual part. |
| GET | /session/:sessionID/v2/entries, /v2/state, /v2/events | The flat event-sourced session projection. See Sessions. |
| GET/POST | /session/:sessionID/context, /context/toggle | Context breakdown and per-source enable/disable — what the TUI context manager drives. |
| GET/POST | /session/:sessionID/background/*, /monitor/* | Inspect, read, and cancel background delegations and long-lived monitors. |
Mobile API
The /mobile group is intentionally larger than the base API because it exposes a
complete phone workflow. Its endpoints are declared in httpapi/mobile.ts; the
implementations are split by concern across server/mobile/ and wired up in
httpapi/mobile-handlers.ts.
| Route area | Examples | Handler |
|---|---|---|
| Pairing and bootstrap | /mobile/auth/token, /mobile/bootstrap | server/mobile/auth.ts, helpers.ts |
| Sessions | List, create, prompt, stream, lifecycle | server/mobile/session.ts, session-lifecycle.ts, events.ts |
| Git and GitHub | Status, diff, commit, repo search and import | server/mobile/git.ts, github.ts, worktree.ts |
| Terminal | PTY create/list/input/resize/close | server/mobile/pty.ts |
| Automation | Loops, missions, routines | server/mobile/loops.ts, missions.ts |
| Teleport | Push a local session to a remote host | server/mobile/teleport.ts |
| Host state | Feature flags, host status, memory | server/mobile/features.ts, host-status.ts, memory.ts |
POST /mobile/auth/token is a better first host smoke than
/mobile/bootstrap because bootstrap touches more subsystems, including project
detection, GitHub, Expo, container runtime, and config.
A single-file server/routes/mobile.ts shadows the split server/mobile/
directory and silently serves an empty session list. If that file reappears, delete it.
The HttpApi bridge
httpapi/bridge.ts turns the declared API into a served one. It builds an Effect
HttpRouter from PublicHttpApi.Api, keeps a machine-readable
RoutePattern table for coverage checks, and exposes two entry points:
handle() for instance-bound requests and handleGlobal() for the
instance-less roots.
supports(path, method)andsupportsGlobal(path, method)decide whether the bridge claims a request; a miss falls through to the proxy.- Path templates (
/session/{sessionID}/message) are compiled to RegExps, with name segments matched as[^/]+. script/check-route-coverage.tsandtest/server/routes-coverage.test.tsassert every entry in the table still matches, so a renamed path fails CI instead of 404ing in production.
Unlike the old Hono handlers, the bridge validates response bodies at runtime. A schema that
is stricter than the real data turns a working route into a 500 — verify new or tightened
response schemas against a live curl, not just against types.
Events and streaming
The server has two event styles. SSE endpoints give clients a long-lived feed; internal
Bus events carry state changes between subsystems in-process.
GET /eventstreams instance-scoped events;GET /global/eventstreams instance-less ones.GET /sync/streamstreams the sync projection feed. See Sync.- The
/tuigroup publishesTuiEvent.CommandExecuteso external clients can open dialogs or run commands in the active TUI. - Permission and question routes publish request/reply events through their services.
- MCP publishes
mcp.tools.changedwhen a connected server sends a tool-list notification. - PTY output rides a websocket rather than SSE, upgraded in
server/websocket.ts.
OpenAPI and SDK generation
Server.openapi() returns OpenApi.fromApi(PublicApi) — the document is
derived from the same typed API that serves the traffic, so it cannot drift from the
implementation. The API reference is rendered from that same document,
endpoint by endpoint.
The JavaScript SDK is generated from that API by
packages/nikcli/script/generate-httpapi-clients.ts, using
@nikcli-ai/httpapi-codegen. One run emits three artifacts:
| Emitter | Output | Consumer |
|---|---|---|
emitPromise | packages/sdk/js/src/httpapi/generated | The public Promise-based SDK (@nikcli-ai/sdk/httpapi). |
emitEffectImported | src/server/httpapi/client/generated | In-process Effect clients. |
emitEffectShape | src/server/httpapi/client/api | Standalone Effect client shapes. |
bun run --cwd packages/nikcli generate:httpapi-clients
# or, equivalently, from the SDK package:
bun run --cwd packages/sdk/js generate:httpapi
Everything under src/httpapi/generated and
server/httpapi/client/ is emitted. Change the endpoint declaration in the
HttpApi group and re-run the generator. Two endpoint families are deliberately skipped by
the generator: websocket upgrades (pty-connect.connect,
mobile.ptyConnect) and union payloads (auth.set,
session.partUpdate).
Failure modes
| Symptom | Likely cause | Check |
|---|---|---|
| 401 on every request | Missing bearer token, Basic Auth password mismatch, or Tailscale identity header absent. | Check auth headers and NIKCLI_SERVER_PASSWORD. |
| Workspace not found | x-nikcli-workspace points at a workspace with no target. | Call workspace list or remove the stale workspace id. |
| Route returns the hosted web app | The bridge does not claim that method/path, so the fallback proxied it to app.nikcli-ai.dev. | Inspect HttpApiBridge.supports and the RoutePattern table. |
| 500 on a route that used to work | A response schema is stricter than the real payload — the bridge validates responses. | curl the route and diff the body against the schema. |
| Empty mobile session list | A resurrected server/routes/mobile.ts shadowing server/mobile/. | Delete the monolith file. |
| CORS preflight fails | Origin is not local, app, Tailscale, nikcli-ai.dev, or whitelisted. | Set NIKCLI_SERVER_CORS_ORIGINS for custom origins. |
| Mobile bootstrap is slow | Bootstrap hits project, GitHub, Expo, container, provider, and config checks. | Smoke /mobile/auth/token first. |
| Unknown error JSON | Error did not match a known domain class in ServerRouter.mapError. | Use --print-logs and inspect Global.Path.log. |
Source anchors
| Need | File |
|---|---|
| Listener, ports, OpenAPI, memory pressure | packages/nikcli/src/server/server.ts |
| Request pipeline, CORS, context, dispatch, error mapping | packages/nikcli/src/server/server-router.ts |
| API composition and handler wiring | packages/nikcli/src/server/httpapi/public.ts |
| Bridge, route table, coverage | packages/nikcli/src/server/httpapi/bridge.ts |
| Raw handlers before the bridge | packages/nikcli/src/server/public.ts, server/extra.ts |
| Instance-less roots | packages/nikcli/src/server/httpapi/instance-less.ts |
| Auth principal resolution | packages/nikcli/src/server/httpapi/auth.ts |
| Typed session API | packages/nikcli/src/server/httpapi/session.ts |
| Prompt streaming | packages/nikcli/src/server/httpapi/prompt.ts |
| Mobile API and handlers | packages/nikcli/src/server/httpapi/mobile.ts, server/mobile/ |
| SSE feeds | packages/nikcli/src/server/httpapi/event.ts, event-feed.ts |
| Websocket upgrades | packages/nikcli/src/server/websocket.ts |
| Workspace proxying | packages/nikcli/src/server/proxy.ts, packages/nikcli/src/workspace |
| SDK generation | packages/nikcli/script/generate-httpapi-clients.ts, packages/httpapi-codegen |