docs

Quickstart

JavaScript

Everything is fetch. The one wrinkle is live reads: the subscribe endpoints are POSTs (the filter body does not fit a query string), so the browser's EventSource does not apply — you read the response body as a stream and split SSE frames yourself. Below is the whole loop in ~60 lines, runnable in Node 18+ or a browser.

const API = "http://127.0.0.1:7001/v1";

async function call(method, path, body) {
  const res = await fetch(API + path, {
    method,
    headers: body ? { "content-type": "application/json" } : {},
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const { error } = await res.json();            // uniform shape: {error:{code,message,details?}}
    throw new Error(`${res.status} ${error.code}: ${error.message}`);
  }
  return res.status === 204 ? null : res.json();
}

1. Create a space

const space = await call("POST", "/spaces", { name: "Notebook" });
const SPACE = space.id;                              // "bafyreig…"

2. Create an object

const { objectId } = await call("POST", `/spaces/${SPACE}/objects`, {
  types: ["page"],
  initialProperties: { any: { name: "Reading list" } },
});

3. Query

const page = await call("POST", `/spaces/${SPACE}/objects/query`, {
  filter: { "any.types": "page" },
  sort: ["-modifiedAt"],
  limit: 20,
  includeTotal: true,
});
console.log(page.total, page.records.map(r => r.any.name));
// timestamps arrive as {"$date": "…"}:
const modified = new Date(page.records[0].modifiedAt.$date);

4. Subscribe

One POST returns text/event-stream. Parse it frame by frame: frames are separated by a blank line, each has event: and data: lines, and : keepalive comments can be ignored.

async function* sse(path, body, signal) {
  const res = await fetch(API + path, {
    method: "POST", signal,
    headers: { "content-type": "application/json", accept: "text/event-stream" },
    body: JSON.stringify(body),
  });
  if (!res.ok) { const { error } = await res.json(); throw new Error(error.code); }
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buf = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) return;
    buf += value;
    let i;
    while ((i = buf.indexOf("\n\n")) >= 0) {
      const frame = buf.slice(0, i); buf = buf.slice(i + 2);
      let event = "message", data = "";
      for (const line of frame.split("\n")) {
        if (line.startsWith("event:")) event = line.slice(6).trim();
        else if (line.startsWith("data:")) data += line.slice(5).trim();
      }
      if (data) yield { event, data: JSON.parse(data) };
    }
  }
}

const ctl = new AbortController();
const window = new Map();                            // id → record: hold a window, not a database

for await (const { event, data } of sse(`/spaces/${SPACE}/objects/query/subscribe`,
    { filter: { "any.types": "page" }, sort: ["-modifiedAt"], limit: 20 }, ctl.signal)) {
  if (event === "ready") continue;                   // stream is live after this
  if (event === "snapshot") { for (const r of data.records) window.set(r.id, r); render(); }
  if (event === "changes") {
    for (const ch of data) {
      for (const r of ch.added)   window.set(r.id, r.doc);
      for (const r of ch.updated) window.set(r.id, r.doc);
      for (const r of ch.removed) window.delete(r.id);
    }
    render();
  }
  if (event === "closed") break;                     // terminal: reopen for a fresh snapshot
}

Rename the object from another tab or with curl and watch an updated entry arrive:

await call("POST", `/spaces/${SPACE}/properties/${objectId}/set/any`, { name: "Reading list 2026" });

Recovery

closed carries a reason — server_shutdown, sdk_closed, overflow (you drained too slowly), drifted (too much of the window left). All four mean the same thing: open a new POST and replace your window with the new snapshot. There is no replay and nothing to reconcile (Subscriptions).

Writing and reading back

Writes return {versionId, changeId, recordIds} and never the record. Read it back through a query, or let the open subscription deliver it — stamp versionId on what you wrote if you need to recognise your own change on the stream (Best practices).

const r = await call("POST", `/spaces/${SPACE}/objects/${chatId}/chat/messages`, { text: "hello" });
r.recordIds[0];                                     // the new message id

Note. In a browser the server's CORS allowlist covers the desktop-shell webview origins and the Vite dev origins; a page served from another origin will be blocked by the browser even though the server is on loopback. Serve your dev page from Vite, or proxy /v1 through your dev server (Security model).

Next: Python, or on to Reading data.