Skip to content
nikcli
Docs
Documentation

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.

There is no Hono layer any more

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.

httpapi/*.ts

Typed HttpApi groups

One HttpApiGroup per domain — session, file, provider, mcp, mobile, sync, loop, mission, analytics — with schema-checked request and response bodies.

public.ts / extra.ts

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.

Context

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.

httpapi/auth.ts

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.

Terminal
bash
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
ModeCommandPrimary routesNotes
Local APInikcli serveAll mounted server routesSafe for loopback development. Password is optional on loopback.
Public APInikcli serve --hostname 0.0.0.0All mounted server routesSet NIKCLI_SERVER_PASSWORD or use loopback Tailscale identity mode.
Mobile hostnikcli mobile serve --pair/mobile/*, /user/*, shared project/session routesBearer token auth is minted by the mobile pairing commands.
Workspace hostnikcli workspace-serve/experimental/workspace/* plus the standard APIHosts workspace contexts, status, events, and session proxying.
TUI externalnikcli --port 0TUI worker server plus standard APIUsed when the terminal UI needs network-visible access.
Port fallback is automatic

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:

  1. Body limit. Oversized bodies are rejected before anything is parsed (middleware/body-limit.ts).
  2. CORS preflight. OPTIONS short-circuits with the computed CORS headers.
  3. Public share routes. /s/:shareID, /share/:shareID, and /api/share/:shareID answer before any auth check.
  4. Auth. Auth.authenticate() resolves a principal from mobile tokens, account tokens, Tailscale identity, or Basic Auth. Public paths are allowed through unauthenticated.
  5. Global vs instance split. isInstanceLessPath() routes /global, /user, /account (plus the /sync/stream SSE feed) to the global branch, which never binds a project directory.
  6. Context resolution. Everything else resolves a directory and workspace, proxies remote workspaces through ServerProxy, and enters WorkspaceContext.provide + withInstanceAsync.
  7. Websocket upgrades. PTY connect paths upgrade in place instead of returning a body.
  8. Raw handlers. PublicRoutes.instanceRequest() claims SSE, prompt streaming, chatbot webhooks, and the extra.ts routes.
  9. HttpApi bridge. If HttpApiBridge.supports(path, method) matches, the typed handler answers.
  10. Fallback. Unmatched non-/mobile paths are proxied to app.nikcli-ai.dev so the hosted web app can be served from the same origin.
Raw before typed, always

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().

LayerAccepted inputBehavior
Public paths/user/login, /user/register, /user/status, share routesPublic by design so accounts can be created and login status can be checked.
Account tokenAuthorization: Bearer nku_…Resolves the local account record; /account/* and /user/* run instance-less.
Mobile tokenAuthorization: Bearer or ?token=Verified against hashed tokens in the mobile auth table; required for /mobile/* when mobileAuthRequired is set.
Tailscale identityTailscale-User-LoginTrusted only on a loopback listener with NIKCLI_SERVER_TAILSCALE_AUTH; allows users from NIKCLI_SERVER_TAILSCALE_USERS.
Basic AuthUsername and passwordUsername defaults to nikcli; password comes from NIKCLI_SERVER_PASSWORD.
Terminal
bash
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
Tailscale identity needs loopback

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.

InputPriorityEffect
x-nikcli-workspace / ?workspace=HighestPins the workspace and skips the session lookup entirely.
directory queryHighSets the instance directory unless a workspace target overrides it.
x-nikcli-directoryHighSame as query; used by the SDK and mobile clients.
Session route idFallbackFor /session/:id… paths the server recovers the session's directory and workspace.
Instance-less rootsBypass/global, /user, /account never bind a directory.
Instance-less roots are a single table

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.

PrefixSourcePurpose
/sessionhttpapi/session.tsSession lifecycle, messages, parts, revert, share, todos, diffs, context breakdown, background delegations, monitors, v2 entries.
/projecthttpapi/project.tsProject list, current project, and project updates.
/confighttpapi/config.tsRead and update config plus the provider summary.
/providerhttpapi/provider.tsProvider catalog, auth methods, API token write, auth removal.
/mcphttpapi/mcp.tsMCP status, add, OAuth start/callback/authenticate, connect, disconnect, toggle.
/permissionhttpapi/permission.tsPending permission requests and replies.
/questionhttpapi/question.tsUser question requests, replies, and rejections.
/ptyhttpapi/pty.tsPTY lifecycle: create, list, input, resize, close. The websocket connect path upgrades in the router.
/experimentalhttpapi/experimental.tsTool metadata and ids, worktrees, and other unstable surfaces.
/experimental/workspacehttpapi/workspace.tsAdaptors, create/list/remove/restore, status, session restore, warp.
/mobilehttpapi/mobile.ts, handlers in server/mobile/Mobile bootstrap, sessions, repos, git, GitHub, approvals, routines, loops, missions, terminals, teleport, memory.
/connectorshttpapi/connectors.tsConnector status, auth, logout, cache invalidation.
/chatbothttpapi/chatbot-api.tsBot config CRUD, start/stop, and delivery state.
/discordhttpapi/discord.tsDiscord bot setup and lifecycle.
/brainhttpapi/brain.tsBrain model selection, scheduler state, and manual triggers.
/missionhttpapi/mission.tsMission list, templates, generate, validation review, and run endpoints.
/loophttpapi/loop.tsLoops CRUD, templates, generate, run, pause, resume, schedule.
/synchttpapi/sync.tsEvent push, outbox, snapshots, stats, config, connect/disconnect/drain. See Sync.
/analyticshttpapi/analytics.tsGlobal, daily, and per-session analytics summaries and drilldowns.
/doctorhttpapi/doctor.tsHealth, connectivity, and runtime diagnostics behind nikcli doctor.
/profilehttpapi/profile.tsDeclared user profile and learned habits.
/voicehttpapi/voice.tsAudio transcription for voice input.
/tuihttpapi/tui.tsRemote control: append/submit/clear prompt, open dialogs, execute commands, toasts, control channel.
/globalhttpapi/global.tsInstance-less health and dispose. /global/event is the raw SSE feed.
/userhttpapi/users.tsLocal account login, registration, profile, and user admin. Instance-less.
/accounthttpapi/account.tsnikcli 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.tsPOST /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.

PathHandlerWhy it is raw
GET /eventhttpapi/event.tsLong-lived instance SSE feed.
GET /global/eventhttpapi/event.tsLong-lived instance-less SSE feed.
GET /sync/streamhttpapi/sync.tsSync SSE feed; the only /sync/ path served instance-less.
POST /session/:id/messagehttpapi/prompt.tsStreams assistant output as it is produced.
POST /session/:id/prompt_asynchttpapi/prompt.tsFire-and-forget prompt submission.
/chatbot/* webhookshttpapi/chatbot.tsProvider-defined bodies and signature verification; falls through so /chatbot/bots* stays typed.
/auth/:providerID, /config/reload, /config/mcp, /config/profilesserver/extra.tsUnion payloads and mutations the encoded router cannot express yet.
/pty/:ptyID/connectserver/websocket.tsWebsocket upgrade.
GET /companionserver/companion.tsServes the embedded companion HTML shell.
/share/:shareID, /s/:shareIDserver/public.tsMust 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.

MethodPathBehavior
GET/sessionList sessions, filtered by directory, roots, start timestamp, search, and limit.
POST/sessionCreate a session with optional parent, title, permission rules, skills, GitHub metadata, and workspace id.
GET/session/statusReturn the current session status map.
GET/PATCH/session/:sessionIDRead or update title and archive time.
POST/session/:sessionID/forkFork from a session, optionally at a message id.
POST/session/:sessionID/abortCancel prompt execution, owned delegation jobs, and monitors.
POST/session/:sessionID/revert / /unrevertRevert the diff associated with a message or part, or undo the previous revert.
POST/session/:sessionID/summarizeCompact the transcript into a summary.
POST/session/:sessionID/command, /shellRun a prompt command or a shell command inside the session.
GET/session/:sessionID/messageList 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/eventsThe flat event-sourced session projection. See Sessions.
GET/POST/session/:sessionID/context, /context/toggleContext 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 areaExamplesHandler
Pairing and bootstrap/mobile/auth/token, /mobile/bootstrapserver/mobile/auth.ts, helpers.ts
SessionsList, create, prompt, stream, lifecycleserver/mobile/session.ts, session-lifecycle.ts, events.ts
Git and GitHubStatus, diff, commit, repo search and importserver/mobile/git.ts, github.ts, worktree.ts
TerminalPTY create/list/input/resize/closeserver/mobile/pty.ts
AutomationLoops, missions, routinesserver/mobile/loops.ts, missions.ts
TeleportPush a local session to a remote hostserver/mobile/teleport.ts
Host stateFeature flags, host status, memoryserver/mobile/features.ts, host-status.ts, memory.ts
Prefer narrow mobile smokes

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.

Never resurrect routes/mobile.ts

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) and supportsGlobal(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.ts and test/server/routes-coverage.test.ts assert every entry in the table still matches, so a renamed path fails CI instead of 404ing in production.
Strict schemas validate responses too

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 /event streams instance-scoped events; GET /global/event streams instance-less ones.
  • GET /sync/stream streams the sync projection feed. See Sync.
  • The /tui group publishes TuiEvent.CommandExecute so 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.changed when 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:

EmitterOutputConsumer
emitPromisepackages/sdk/js/src/httpapi/generatedThe public Promise-based SDK (@nikcli-ai/sdk/httpapi).
emitEffectImportedsrc/server/httpapi/client/generatedIn-process Effect clients.
emitEffectShapesrc/server/httpapi/client/apiStandalone Effect client shapes.
Terminal
bash
bun run --cwd packages/nikcli generate:httpapi-clients
# or, equivalently, from the SDK package:
bun run --cwd packages/sdk/js generate:httpapi
Do not hand-edit generated files

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

SymptomLikely causeCheck
401 on every requestMissing bearer token, Basic Auth password mismatch, or Tailscale identity header absent.Check auth headers and NIKCLI_SERVER_PASSWORD.
Workspace not foundx-nikcli-workspace points at a workspace with no target.Call workspace list or remove the stale workspace id.
Route returns the hosted web appThe 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 workA 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 listA resurrected server/routes/mobile.ts shadowing server/mobile/.Delete the monolith file.
CORS preflight failsOrigin is not local, app, Tailscale, nikcli-ai.dev, or whitelisted.Set NIKCLI_SERVER_CORS_ORIGINS for custom origins.
Mobile bootstrap is slowBootstrap hits project, GitHub, Expo, container, provider, and config checks.Smoke /mobile/auth/token first.
Unknown error JSONError did not match a known domain class in ServerRouter.mapError.Use --print-logs and inspect Global.Path.log.

Source anchors

NeedFile
Listener, ports, OpenAPI, memory pressurepackages/nikcli/src/server/server.ts
Request pipeline, CORS, context, dispatch, error mappingpackages/nikcli/src/server/server-router.ts
API composition and handler wiringpackages/nikcli/src/server/httpapi/public.ts
Bridge, route table, coveragepackages/nikcli/src/server/httpapi/bridge.ts
Raw handlers before the bridgepackages/nikcli/src/server/public.ts, server/extra.ts
Instance-less rootspackages/nikcli/src/server/httpapi/instance-less.ts
Auth principal resolutionpackages/nikcli/src/server/httpapi/auth.ts
Typed session APIpackages/nikcli/src/server/httpapi/session.ts
Prompt streamingpackages/nikcli/src/server/httpapi/prompt.ts
Mobile API and handlerspackages/nikcli/src/server/httpapi/mobile.ts, server/mobile/
SSE feedspackages/nikcli/src/server/httpapi/event.ts, event-feed.ts
Websocket upgradespackages/nikcli/src/server/websocket.ts
Workspace proxyingpackages/nikcli/src/server/proxy.ts, packages/nikcli/src/workspace
SDK generationpackages/nikcli/script/generate-httpapi-clients.ts, packages/httpapi-codegen