nimbus

Use the native HTTP and WebSocket API

Talk to Nimbus from any language — create tenants, write documents, run queries, and subscribe to live results over WebSocket.

Nimbus speaks plain HTTP and WebSocket, so any language with an HTTP client can use it without an SDK or codegen. This guide walks through authenticating, scoping requests to a tenant, working with documents, and subscribing to live query results.

If you have not run a server yet, start with the self-host quickstart.

1. Get the local admin token

A server started with nimbus start protects its native API with a local admin token. Nimbus creates the token on first boot and stores it as a JSON file:

PlatformToken file
Linux~/.local/share/nimbus/auth/token
macOS~/Library/Application Support/nimbus/auth/token
Windows%LOCALAPPDATA%\nimbus\auth\token.json

Read the token field and export it for the rest of this guide:

# Linux
export NIMBUS_TOKEN=$(jq -r .token ~/.local/share/nimbus/auth/token)

# macOS
export NIMBUS_TOKEN=$(jq -r .token "$HOME/Library/Application Support/nimbus/auth/token")

Send it on every request, either as a bearer token or in the X-Nimbus-Admin-Token header:

curl -s http://localhost:8080/api/tenants \
  -H "Authorization: Bearer $NIMBUS_TOKEN"

Requests without a valid credential get a 401 with code auth.unauthorized. Browser-based callers must also use loopback origins. See the HTTP API reference for the full access rules.

2. Scope requests to a tenant

The URL path scopes every data operation to a tenant: /api/tenants/{tenant_id}/.... Create a tenant first:

curl -s -X POST http://localhost:8080/api/tenants \
  -H "Authorization: Bearer $NIMBUS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id": "demo"}'

The server replies 201 Created with {"id": "demo"}. Tenants are fully isolated from each other: documents, schemas, scheduled jobs, and subscriptions never cross tenant boundaries.

3. Insert and read documents

Insert a document by naming a table and its fields. Nimbus creates tables implicitly on the first write:

curl -s -X POST http://localhost:8080/api/tenants/demo/documents \
  -H "Authorization: Bearer $NIMBUS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"table": "messages", "fields": {"text": "hello world", "author": "you"}}'

The response is 201 Created with the generated document id:

{"id": "01jx2x9w7d2f0v6q8t3k5r9e1b"}

Read it back. Single documents and table listings are GET requests:

# One document
curl -s http://localhost:8080/api/tenants/demo/documents/messages/<id> \
  -H "Authorization: Bearer $NIMBUS_TOKEN"

# Every document in the table
curl -s http://localhost:8080/api/tenants/demo/documents/messages \
  -H "Authorization: Bearer $NIMBUS_TOKEN"

Returned documents carry three system fields alongside your own: _id, _creationTime, and _updateTime (epoch milliseconds). Update with PATCH (a partial patch object) and delete with DELETE on the same /documents/{table}/{document_id} path.

4. Run queries

POST a query object to filter, order, and limit results. The request requires filters. Pass an empty array to match everything:

curl -s -X POST http://localhost:8080/api/tenants/demo/query \
  -H "Authorization: Bearer $NIMBUS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "table": "messages",
    "filters": [{"field": "author", "op": "eq", "value": "you"}],
    "order": {"field": "_creationTime", "direction": "desc"},
    "limit": 10
  }'

For large result sets, use /query/paginated with a page_size and follow the returned cursor. The HTTP API reference lists both endpoints, all filter operators, and the paginated shapes.

5. Subscribe to live results over WebSocket

You can register the same query as a subscription. The server pushes a fresh snapshot whenever a mutation changes the result. Connect to /ws with the nimbus.v2 subprotocol and identify the tenant with an X-Tenant-Id header or a tenant_id query parameter.

This example uses the ws package, which lets you set headers during the upgrade:

import WebSocket from "ws";

const socket = new WebSocket("ws://localhost:8080/ws?tenant_id=demo", ["nimbus.v2"], {
  headers: { Authorization: `Bearer ${process.env.NIMBUS_TOKEN}` },
});

socket.on("message", (raw) => {
  const frame = JSON.parse(raw.toString());

  switch (frame.type) {
    case "hello":
      // Complete the handshake, then subscribe.
      socket.send(JSON.stringify({ type: "client_hello", protocol: "nimbus.v2" }));
      socket.send(
        JSON.stringify({
          type: "subscribe",
          request_id: "messages-1",
          query: { table: "messages", filters: [] },
        }),
      );
      break;
    case "subscription_result":
      // frame.data is the full current result set for the query.
      console.log(`subscription ${frame.subscription_id}:`, frame.data);
      break;
    case "op.error":
    case "error":
    case "fatal_error":
      console.error(frame.error.code, frame.error.message);
      break;
  }
});

Two handshake rules to respect: reply to the server's hello frame with client_hello within 10 seconds, and send only JSON text frames. The full frame catalog, handshake failure modes, and reconnect semantics are in the WebSocket protocol reference.

Handle errors

HTTP and WebSocket errors use one envelope:

{
  "error": {
    "code": "op.invalid_input",
    "message": "invalid document id `abc`",
    "requestId": "req-...",
    "timestamp": "2026-06-10T17:03:21Z",
    "severity": "error",
    "retryable": false,
    "detail": null,
    "remediation": { "action": "fix_request", "message": "Correct the request payload before retrying." }
  }
}

Branch on code, retry when retryable is true, and surface remediation.message to operators. The complete code catalog with HTTP status mappings is in the error reference.

Next steps

On this page