GETTING STARTED

Build a realtime app from scratch in six steps.

We'll build a small but complete app on OriginDB — a shared notes board: click to pin a note and it appears live on every open tab. You'll write the whole backend as one WebAssembly reducer module, compile it, deploy it into the running database, and wire up a browser client that reads from a SQL-filtered changefeed. No separate API server — the same primitives power the NIGHTBOARD demo.

a reducer module (.wasm) one table, two reducers a live browser client

Prerequisites: a C++17 toolchain + CMake to build the engine, and Node 18+ for the SDK toolchain and the demo's web page. Everything runs on your machine — no cloud account.

1
Install

Get OriginDB and the SDK

Clone the repo. Build the engine — one binary with storage, WAL, changefeed, SQL, and the wasmtime host — plus the origindb CLI. Then install the AssemblyScript toolchain that compiles your module to WASM.

shell
# build the engine → build/origindb (one CLI: serve, deploy, call, exec…)
$ git clone https://github.com/MattPoetker/origindb.git && cd origindb
$ cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j

# install the AssemblyScript SDK toolchain
$ cd sdk/typescript && npm install && cd ../..
2
Write

Write the module — your whole backend

This one file is the entire server. It declares a notes table and exports two reducers — the only way clients change state. addNote writes a row; clearNotes deletes them all. Save it as sdk/typescript/examples/board/index.ts.

examples/board/index.ts
// examples/board/index.ts — the entire backend for a realtime notes wall.
import {
  JsonValue, abortCall, declareTable, deleteTable, generateId,
  nowMs, registerReducer, scanTable, setModuleInfo, writeTable,
} from "../../assembly/index";

// Every module re-exports the ABI surface the SDK implements.
export {
  origindb_alloc, origindb_free, origindb_describe, origindb_invoke, __origindb_abort,
} from "../../assembly/index";

setModuleInfo("board", "1.0.0");
declareTable("notes");                       // key = <id>: { user, text, x, y, color, created }

// addNote(user, text, x, y, color) -> { "id": "..." }
// x/y are 0..1 fractions of the wall, so every screen renders a note in the same spot.
function addNote(args: Array<JsonValue>): JsonValue | null {
  const user  = args.length > 0 && args[0].asString().length > 0 ? args[0].asString() : "anon";
  const text  = args.length > 1 ? args[1].asString() : "";
  const x     = args.length > 2 ? clamp01(args[2].asNumber()) : 0.5;
  const y     = args.length > 3 ? clamp01(args[3].asNumber()) : 0.5;
  const color = args.length > 4 && args[4].asString().length > 0 ? args[4].asString() : "#ffc24b";

  if (text.length == 0) abortCall("addNote: text is required");   // traps -> the whole call rolls back

  const id = generateId().toString();
  writeTable("notes", id, JsonValue.newObject()
    .setString("id", id).setString("user", user).setString("text", text)
    .setNumber("x", x).setNumber("y", y)
    .setString("color", color).setNumber("created", <f64>nowMs())
    .toString());
  return JsonValue.newObject().setString("id", id);   // commit -> WAL -> one changefeed event
}

// clearNotes() -> { "cleared": n }: wipe the wall in one atomic commit.
function clearNotes(args: Array<JsonValue>): JsonValue | null {
  const rows = JsonValue.parse(scanTable("notes", "", 100000));
  let n = 0;
  for (let i = 0; i < rows.length; i++) { deleteTable("notes", rows.at(i).getString("key", "")); n++; }
  return JsonValue.newObject().setNumber("cleared", <f64>n);
}

function clamp01(v: f64): f64 { return v < 0 ? 0 : v > 1 ? 1 : v; }

registerReducer("addNote", addNote, ["user", "text", "x", "y", "color"]);
registerReducer("clearNotes", clearNotes, []);

Reducers are the only writers. Their writes stage in an overlay and apply as one atomic commit — or roll back entirely if the code traps (that's what abortCall does). A successful commit hits the WAL and emits exactly one changefeed event.

3
Compile

Compile it to WebAssembly

Add a tiny build config next to the module, then compile. AssemblyScript emits a compact .wasm — the artifact you deploy. The incremental runtime gives the module a garbage collector; abort is routed to the SDK so traps reach the server log.

examples/board/asconfig.json
{
  "entries": ["./index.ts"],
  "targets": { "release": { "outFile": "../../build/board.wasm", "optimizeLevel": 3 } },
  "options": { "runtime": "incremental", "exportRuntime": true,
    "use": ["abort=assembly/index/__origindb_abort"] }
}
shell
$ cd sdk/typescript
$ npx asc examples/board/index.ts --config examples/board/asconfig.json --target release
✓ build/board.wasm  (30 KB)
4
Run

Start the server

Point the server at a data directory and pick two ports: a websocket port (browsers subscribe here) and a gRPC port (the CLI and your services talk here). The data dir holds the WAL — the crash-recoverable record of every commit.

shell
$ ./build/origindb serve -d ./board_data -p 8787 -g 50051 --no-auth
✓ WebSocket ready on ws://localhost:8787
✓ gRPC ready on localhost:50051

Flags: -d data dir · -p websocket port · -g gRPC port. --no-auth is for local dev; drop it in production and the server issues admin/client tokens instead.

5
Deploy

Deploy the module and call it

Deploy the .wasm — the engine validates and sandboxes it, then its reducers are callable. Call addNote from the CLI and read the row back with plain SQL. Redeploy a new version any time and the rules change live, no downtime.

shell
$ ./build/origindb deploy board sdk/typescript/build/board.wasm 1.0.0
✓ Deployed module 'board' (30213 bytes) — validated, sandboxed

# addNote(user, text, x, y, color)
$ ./build/origindb call board addNote '["Ada","hello!",0.3,0.4,"#4da3ff"]'
 {"id":"1871105572944740356"}

$ ./build/origindb exec "SELECT * FROM notes"
 id=1871105572944740356  user="Ada"  text="hello!"  x=0.3  y=0.4

That addNote committed and fired one changefeed event. Any client subscribed to notes just received it — which is exactly what we wire up next.

6
Serve

Wire up the browser — a working MVP

The page does two things: it subscribes to SELECT * FROM notes over the websocket (snapshot + live commits), and it calls addNote when you click. Reducer calls go through a ~60-line Node bridge (browsers can't speak gRPC); realtime reads flow straight from OriginDB. The full server.js and public/index.html ship in examples/board/ — here's the core:

the read + write loop
// public/index.html <script> — the read + write loop (full file in examples/board/)
const cfg = await fetch("/api/config").then((r) => r.json());

// WRITE: call a reducer through the tiny bridge (browsers can't speak gRPC)
const call = (reducer, args) =>
  fetch("/api/call", { method: "POST", headers: { "content-type": "application/json" },
    body: JSON.stringify({ reducer, args }) });

// READ: subscribe straight to OriginDB's websocket — snapshot + every live commit
const ws = new WebSocket("ws://" + location.hostname + ":" + cfg.wsPort);
ws.onopen = () => ws.send(JSON.stringify({ type: "sql_subscribe", sql: "SELECT * FROM notes" }));
ws.onmessage = (e) => {
  const m = JSON.parse(e.data);
  if (m.type === "initial_state") for (const row of m.rows) renderNote(row.key, row.data);
  else if (m.type === "sql_changefeed_event") {
    if (m.operation === "DELETE") removeNote(m.key);
    else renderNote(m.key, JSON.parse(m.new_value).columns);
  }
};

// pin a note where you click — it appears on every open tab instantly
wall.onclick = (e) => call("addNote",
  ["me", prompt("Note:"), e.clientX / innerWidth, e.clientY / innerHeight, "#4da3ff"]);
shell
$ cd examples/board && npm install
$ node server.js --port 3020 --grpc localhost:50051 --ws-port 8787
✓ board demo on http://localhost:3020  # open it in two tabs — notes sync live

Open two tabs and click the wall. Each note is a reducer call → a commit → a WAL append → a filtered changefeed event → a render on every tab. That's the whole loop — and your first OriginDB app.

WHAT JUST HAPPENED

One call. The whole loop.

Every click is one lap of the same cycle. A client calls a reducer; your WASM runs and commits atomically; the WAL makes it durable; the changefeed streams it to exactly the subscribers whose SQL matches.

01

Call a reducer

A client invokes addNote. Args arrive as JSON; the host freezes the clock for determinism.

gRPC · your .wasm
02

Commit + WAL

Staged writes apply as one transaction, or roll back on a trap. Durable before you hear “ok”.

ACID · group-commit fsync
03

Filtered changefeed

One ordered event per commit, delivered only to subscribers whose WHERE matches.

websocket · SQL WHERE
THE VOCABULARY

Six words, the whole model.

Everything you just used. Learn them once and every app — a notes board, a game, an MMO — is the same shape.

Module

Your backend, compiled to WebAssembly and deployed into the DB. Validated, sandboxed, hot-swappable.

Reducer

A named function your module exports — addNote. The only way to write. One atomic commit or full rollback.

Table

Where state lives — notes. Written only by reducers, queried with real SQL.

Changefeed

The realtime read path. Subscribe with SELECT … WHERE …; get a snapshot plus every matching commit.

WAL

The write-ahead log — the canonical world. Group-commit fsync makes it durable; replay rebuilds on boot.

Client

Any subscriber — a browser, the CLI, a service. It calls reducers and renders the feed. It holds no authority.

Now grow the board.

You have a working realtime app. Add a cursors table and an onlyMine filter, subscribe with a WHERE, hot-swap a new module version — the six steps never change. The SDK and the ABI have the rest.