@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:
pnpm add -E uWebSockets.js@github:uNetworking/uWebSockets.js#v20.58.0Start 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.
import { createNarsil } from '@delali/narsil'
import { createServer } from '@delali/narsil/server'
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.
import { createNarsil } from '@delali/narsil'
import { createServer } from '@delali/narsil/server'
const engine = await createNarsil({ durability: { directory: './narsil-data' } })
const apiToken = process.env.NARSIL_API_TOKEN
const server = createServer(engine, {
host: '127.0.0.1',
port: 7700,
onRequest: (req) => {
if (req.headers.authorization !== `Bearer ${apiToken}`) {
return { status: 401, code: 'UNAUTHORISED', message: 'Invalid or missing bearer token.' }
}
},
})
await server.listen()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.
Open it to browser clients
With the token check in place, bind a routable address so a browser front-end can call the API. Move the host off loopback and add cors so the browser accepts the responses. Replace the loopback host with the routable one, and add the cors block to the options.
import { createNarsil } from '@delali/narsil'
import { createServer } from '@delali/narsil/server'
const engine = await createNarsil({ durability: { directory: './narsil-data' } })
const apiToken = process.env.NARSIL_API_TOKEN
const server = createServer(engine, {
host: '127.0.0.1',
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.' }
}
},
})
await server.listen()Harden for production
A public server needs 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 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.
import { createNarsil } from '@delali/narsil'
import { createServer } from '@delali/narsil/server'
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: {
maxBodyBytes: 16 * 1024 * 1024,
maxConcurrentRequests: 64,
},
instanceId: process.env.HOSTNAME,
})
await server.listen()Two further options cover needs beyond this file. Register named 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/{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, 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. |
| 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, and the suggest body takes prefix and limit. The HTTP server example documents every endpoint with request and response bodies, curl walkthroughs, Docker packaging, and an environment-driven production launcher.
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.