> Complete content index: https://narsil.sondelali.com/llms.txt

# HTTP server

> Serve any Narsil engine over REST with health probes, batch imports, snapshots, and admin endpoints, secured by your own request hook.

URL: https://narsil.sondelali.com/docs/http-server
Section: Operations
Version: 0.2 (@delali/narsil@0.2.2, latest)

`@delali/narsil/server` wraps an engine you build in a REST API. You own the engine and its configuration, including durability, embedding adapters, and workers. The server uses it across requests. This page builds one file, `server.ts`, step by step. You start a minimal loopback server, add an authentication hook, and open it to browser clients with CORS.

## Install the peer dependency

The HTTP layer runs on `uWebSockets.js`, an optional peer dependency. Install it alongside Narsil before you create a server:

```bash pm
pnpm add -E uWebSockets.js@github:uNetworking/uWebSockets.js#v20.58.0
```

## Start a server

Build the engine first, then pass it to `createServer` with a `host` and `port`. Start `server.ts` with a durable engine bound to the loopback address on port 7700.

```ts title="server.ts"

const engine = await createNarsil({ durability: { directory: './narsil-data' } })

const server = createServer(engine, {
  host: '127.0.0.1',
  port: 7700,
})

await server.listen()
```

## Authenticate every request

A loopback server accepts any caller on the machine, but a shared server must check who is calling. The `onRequest` hook runs before every request. Return a denial object with a `status`, `code`, and `message` to reject the call, or return nothing to allow it. Add a hook to `server.ts` that checks a bearer token and rejects anything that does not match.

```ts title="server.ts"

const engine = await createNarsil({ durability: { directory: './narsil-data' } })

const apiToken = process.env.NARSIL_API_TOKEN // [!code ++]

const server = createServer(engine, {
  host: '127.0.0.1',
  port: 7700,
  onRequest: (req) => { // [!code ++:5]
    if (req.headers.authorization !== `Bearer ${apiToken}`) {
      return { status: 401, code: 'UNAUTHORISED', message: 'Invalid or missing bearer token.' }
    }
  },
})

await server.listen()
```

<Warning>
  The server refuses to bind a non-loopback address without an `onRequest` hook, because the admin endpoints can
  destroy data. `allowInsecure` overrides that check for trusted private networks only.
</Warning>

## Open it to browser clients

With the token check in place, bind a routable address so that a browser front-end can call the API. Move the host off loopback and add `cors` so that the browser accepts the responses. Replace the loopback host with the routable one, and add the `cors` block to the options.

```ts title="server.ts"

const engine = await createNarsil({ durability: { directory: './narsil-data' } })

const apiToken = process.env.NARSIL_API_TOKEN

const server = createServer(engine, {
  host: '127.0.0.1', // [!code --]
  host: '0.0.0.0', // [!code ++]
  port: 7700,
  cors: { // [!code ++:4]
    origin: 'https://search.example.com',
    headers: ['Content-Type', 'Authorization'],
  },
  onRequest: (req) => {
    if (req.headers.authorization !== `Bearer ${apiToken}`) {
      return { status: 401, code: 'UNAUTHORISED', message: 'Invalid or missing bearer token.' }
    }
  },
})

await server.listen()
```

## Harden for production

A public server requires two more guards. `limits` restricts how much work one caller can demand: an oversized request body is rejected with `413`, and once in-flight requests reach `maxConcurrentRequests`, the server rejects the excess with `503` instead of overloading the engine. `instanceId` gives this process a stable name so that after a restart it identifies the long-running tasks it started and marks any that were still running as failed instead of leaving them stuck. In a container, the pod or container name in `HOSTNAME` is a good source. Add both to `server.ts`.

```ts title="server.ts"

const engine = await createNarsil({ durability: { directory: './narsil-data' } })

const apiToken = process.env.NARSIL_API_TOKEN

const server = createServer(engine, {
  host: '0.0.0.0',
  port: 7700,
  cors: {
    origin: 'https://search.example.com',
    headers: ['Content-Type', 'Authorization'],
  },
  onRequest: (req) => {
    if (req.headers.authorization !== `Bearer ${apiToken}`) {
      return { status: 401, code: 'UNAUTHORISED', message: 'Invalid or missing bearer token.' }
    }
  },
  limits: { // [!code ++:4]
    maxBodyBytes: 16 * 1024 * 1024,
    maxConcurrentRequests: 64,
  },
  instanceId: process.env.HOSTNAME, // [!code ++]
})

await server.listen()
```

Two further options cover needs beyond this file. Register named [embedding adapters](/docs/embedding-adapters) under `embeddingAdapters` when a JSON `createIndex` request references an embedding function by name, because functions cannot be serialised in JSON. Pass a `taskStore` backed by Redis, DynamoDB, or a database when long-running task status must persist across restarts and be shared across instances; the default store keeps that status in memory only.

## Endpoint surface

| Area | Endpoints |
| --- | --- |
| Health | `GET /livez`, `GET /readyz`, and `GET /health` report liveness and readiness without authentication, and `GET /version` reports the build identity. |
| Indexes | `POST /indexes`, `GET /indexes`, and `DELETE /indexes/{name}` manage indexes. `GET /indexes` reports `analysisStale` on each index whose terms an earlier analysis produced. `GET /indexes/{name}/stats`, `GET /indexes/{name}/partitions`, and `GET /indexes/{name}/count` report on one index, and `POST /indexes/{name}/_clear` empties it. |
| Documents | `POST /indexes/{name}/documents` inserts, and `GET`, `PUT`, `PATCH`, and `DELETE` on `/indexes/{name}/documents/{id}` read, upsert, update, and remove. `GET /indexes/{name}/documents/{id}/_exists` checks whether a document exists. |
| Bulk | `POST /indexes/{name}/documents/_batch` runs batch writes with partial results, `POST /indexes/{name}/documents/_multi-get` fetches many ids, `POST /indexes/{name}/documents/_list` pages through every stored document, in document-id order or in an order the body names, and `POST /indexes/{name}/documents/_import` streams an NDJSON corpus in bounded batches. |
| Search | `POST /indexes/{name}/search`, `POST /indexes/{name}/search/preflight`, and `POST /indexes/{name}/suggest` run queries, match counts, and autocomplete. Each response carries `analysisStale: true` while the index answers from terms an earlier analysis produced. |
| Operations | `/indexes/{name}/_checkpoint`, `/indexes/{name}/snapshot`, `/indexes/{name}/restore`, `/indexes/{name}/vector-maintenance`, `/indexes/{name}/vectors/_compact`, `/indexes/{name}/vectors/_optimize`, `/indexes/{name}/_rebalance`, and `/indexes/{name}/partition-config` cover the operational surface. `GET /stats/memory` reports engine memory, and `GET /tasks` with `GET /tasks/{id}` report long-running task status. |

The request bodies follow the embedded API: the index config in `POST /indexes` accepts `surfaceForms`, the search body carries the same query params as `query`, including `prefix` for [search as you type](/docs/full-text-search), and the suggest body takes `prefix` and `limit`. The listing body carries the `cursor`, `limit`, `filters`, `sort`, and `document` that [`listDocuments`](/docs/indexes-and-documents#page-through-every-document) takes. That endpoint requires a body, so send `{}` for the first page. `limits.maxFetchDocuments` caps that `limit` at 10,000 documents. A larger value comes back as a 400 `INVALID_REQUEST`, and so does a `sort` naming more than eight fields. The [HTTP server example](https://github.com/assetcorp/narsil/blob/main/packages/ts/examples/http-server/README.md) documents every endpoint with request and response bodies, curl walkthroughs, Docker packaging, and an environment-driven production launcher.

No request sets `analysisStale`. An index whose language module changed keeps serving results, and the engine adds that flag so that a REST client can separate those results from current ones. No endpoint starts a rebuild, so set `analysis` on the engine before you pass it to `createServer` and call `rebuildAnalysis` from the same launcher. [Language support](/docs/language-support#keep-an-index-current-when-a-language-changes) covers both.

## Cluster mode

`@delali/narsil/distribution` holds the building blocks of multi-node cluster mode: node roles, replication, coordinator adapters, and query routing. The distribution layer is experimental and under active development. It currently runs only in-process and its APIs change without notice, so treat it as a preview, not a deployment target. The design is specified in the repository's [distribution spec](https://github.com/assetcorp/narsil/tree/main/packages/spec/distribution).
