Module
Your backend, compiled to WebAssembly and deployed into the DB. Validated, sandboxed, hot-swappable.
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.
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.
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.
# 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 ../..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 — 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.
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.
{
"entries": ["./index.ts"],
"targets": { "release": { "outFile": "../../build/board.wasm", "optimizeLevel": 3 } },
"options": { "runtime": "incremental", "exportRuntime": true,
"use": ["abort=assembly/index/__origindb_abort"] }
}$ cd sdk/typescript
$ npx asc examples/board/index.ts --config examples/board/asconfig.json --target release
✓ build/board.wasm (30 KB)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.
$ ./build/origindb serve -d ./board_data -p 8787 -g 50051 --no-auth
✓ WebSocket ready on ws://localhost:8787
✓ gRPC ready on localhost:50051Flags: -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.
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.
$ ./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.4That addNote committed and fired one changefeed event. Any client subscribed to notes just received it — which is exactly what we wire up next.
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:
// 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"]);$ 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 liveOpen 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.
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.
A client invokes addNote. Args arrive as JSON; the host freezes the clock for determinism.
Staged writes apply as one transaction, or roll back on a trap. Durable before you hear “ok”.
One ordered event per commit, delivered only to subscribers whose WHERE matches.
Everything you just used. Learn them once and every app — a notes board, a game, an MMO — is the same shape.
Your backend, compiled to WebAssembly and deployed into the DB. Validated, sandboxed, hot-swappable.
A named function your module exports — addNote. The only way to write. One atomic commit or full rollback.
Where state lives — notes. Written only by reducers, queried with real SQL.
The realtime read path. Subscribe with SELECT … WHERE …; get a snapshot plus every matching commit.
The write-ahead log — the canonical world. Group-commit fsync makes it durable; replay rebuilds on boot.
Any subscriber — a browser, the CLI, a service. It calls reducers and renders the feed. It holds no authority.
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.