Ports and wire formats

The exact contract for driving a notebook from outside its own UI — over HTTP (the served binary) or over the in-page JS object (the WASM build). Everything a durable driver needs: request bodies, response schemas, the event shape, and what is and isn’t guaranteed.

A notebook exposes one port in two directions: you set a leaf (data in) and you subscribe to cells (data out). Every transport is a projection of that single contract — the same two operations whether the counterparty is a human at a slider, a feed driver, or another program. See live feeds for the pattern this enables and the JS client for a typed wrapper over the browser side.

HTTP (the served binary: notebook run, or a built binary served)

The server binds 127.0.0.1:8080 by default (--addr to change it) and serves four endpoints. It is an unauthenticated local endpoint — see Security before exposing it.

POST /set — edit a leaf

Request body:

{"leaf": "c", "value": 40}

Atomic multi-leaf edit

To change several leaves together, post a values map instead of a single leaf/value:

{"values": {"principal": 250000, "rate": 0.065, "term": 30}}

GET /events — subscribe to cell updates (SSE)

A text/event-stream. On connect, the server replays the current committed state to the new client only — the latest event for each cell, so a freshly opened page paints immediately without a blank frame. It does not run a fresh wave to do this, so a second tab connecting neither re-runs the notebook’s cells nor pushes a redundant repaint to clients already connected. (Only the very first connection, before any wave has run, triggers the initial wave to produce that state.) Each event is one line:

data: {"epoch":7,"cell":"hourlyCost","state":"done","mime":"text/plain","data":"40.24"}

The JSON payload is the frozen wire-event shape:

Field Type Meaning
epoch number the wave this event belongs to; monotonic, bumped per edit
cell string the cell id (empty on the wave-settled marker — see below)
state string running | done | error | blocked | stale | settled
mime string present on done with rendered output (image/svg+xml, text/html, text/markdown, text/plain)
data string the rendered payload for that MIME; omitted when empty
err string present only on state: "error"

mime/data/err are omitted when empty (omitempty), so a bare transition carries only epoch/cell/state.

The wave-settled marker. When a wave has run every cell to completion without being superseded by a newer edit, the stream emits one terminal event with an empty cell:

data: {"epoch":7,"cell":"","state":"settled"}

This tells a program the wave is coherent and complete — every cell that was going to update in epoch 7 has. A superseded wave never emits it (the newer wave will), so a consumer buffering by epoch can flush its set on settled and know it holds one wave’s values, never a mix. The default UI ignores it (it keys on a cell that does not exist). This is the SSE parallel of the JS client’s subscribeEpoch.

GET /leaves — current leaf values

Returns a JSON object of leaf cells only (inputs), keyed by result name, for seeding controls:

{"c": 80, "price": 1.006, "target": 0.2}

Derived cells (e.g. hourlyCost) are not here — observe those on /events. If no wave has run yet, one is run so defaults exist.

GET / — the built-in UI

The HTML page that mounts the default client. A host driving the notebook programmatically ignores this and uses /set + /events.

Guarantees and non-guarantees

Browser (the WASM build: globalThis.notebook)

A WASM notebook publishes one plain-data object, globalThis.notebook, with the same one-port contract. All values cross as plain JS (numbers, bools, arrays, objects) — JS never sees a Go type.

notebook.meta                 // CellMeta[] — the graph, labels, leaf symbols, widget kinds, leaf types
notebook.provenance           // build identity (source hash, commit)
notebook.set(leaf, value)     // edit a leaf (data in) — same coercer as POST /set
notebook.subscribe(fn)        // rendered events {epoch,cell,state,mime,data,err} → returns unsubscribe
notebook.subscribeValues(fn)  // TYPED value events {epoch,cell,value} + {epoch,settled} → returns unsubscribe
notebook.values()             // synchronous snapshot of every leaf's current value
notebook.start()              // run the first wave, so cells paint their defaults

Security

The built-in server is a local development and trusted-network endpoint, not a multi-user service:

Before any network exposure, place authentication and TLS in front of it (a reverse proxy — nginx, Caddy — terminating TLS and enforcing auth), and treat a served notebook as a trusted local service. A notebook you did not write should be treated like any other untrusted program: it is compiled Go with full host access in the served/native topologies (the WASM topology is sandboxed by the browser). See also Rendering is trusted code.