# 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
```

$ shell · paste into your terminal

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

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

| 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`. |

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
```

$ shell · paste into your terminal

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.

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

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`.

| 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](/docs/loops) CRUD, templates, generate, run, pause, resume, schedule. |
| `/sync` | `httpapi/sync.ts` | Event push, outbox, snapshots, stats, config, connect/disconnect/drain. See [Sync](/docs/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](/docs/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` |

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](/docs/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](/docs/api) 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. |

Terminal

bash 

```
bun run --cwd packages/nikcli generate:httpapi-clients
# or, equivalently, from the SDK package:
bun run --cwd packages/sdk/js generate:httpapi
```

$ shell · paste into your terminal

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

| 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` |

---

Source: https://nikcli-ai.dev/docs/server-api
