← All guided builds

Guided build · 09

Follow one model over realtime events

Subscribe over SSE, adopt the initial snapshot, order revisions, detect stale events, and reconnect through explicit resynchronization.

You will finish with: A resilient event-consumer workflow that treats SSE as invalidation plus synchronization rather than a gap-free value log.

25 minIntermediateSource checked for Grid 0.61.0Reviewed 2026-08-26
Realtime modelrealtime-revenue.gridA small protected revenue model for the live input change.
SSE fixturesample-stream.sseA deterministic stream transcript with synchronization, heartbeat, and update events.

Watch it in Grid

See the workflow before you build it.

Follow the finished interaction, then use the written steps below to build and inspect it yourself.

Companion film

Replay

Walk through edits and rule firings as an event stream, inspect the past, then return to unchanged live state.

23 secGrid 0.61.0
Open film page
On this page

What you will build

You will subscribe to one Grid model over Server-Sent Events (SSE), synchronize its initial state, make a deliberate input change, and recover after intentionally missing an event. The finished client loop treats reconnect as resynchronization—not as gap-free replay.

The included SSE transcript is a deterministic local fixture. It is not a capture from a Runtime Host and is not a runtime execution receipt.

Before you begin

You need:

  • a running Grid Server Build whose /readyz response is 200 and status: "ok";
  • a public API bearer token, curl, and jq;
  • two terminal windows;
  • the assets above saved as realtime-revenue.grid and sample-stream.sse.
export GRID_URL="http://localhost:3000"
export GRID_TOKEN="replace-with-your-api-token"

Use only the Node API. Node owns the SSE connection and fan-out; Runtime Host remains private and owns model execution. Browser EventSource cannot add an Authorization header, so these terminal commands use curl. For a browser client, use same-origin session authentication or a scoped server-side proxy as described under browser authentication.

1. Parse the local SSE fixture

An SSE stream is a sequence of fields separated by blank lines, not NDJSON. Inspect the included transcript:

sed -n 's/^data: //p' sample-stream.sse \
  | jq -r '.type // "heartbeat"'

Deterministic local checkpoint:

stream:hello
model:snapshot
heartbeat
symbol:update

The named heartbeat event contains {} and therefore has no top-level type. A production parser must retain the SSE event name instead of treating every data: line as the same message class.

2. Deploy the temporary model

Create the request from the exact file and deploy it in protected mode:

jq -n --rawfile source realtime-revenue.grid '{
  id: "realtime-revenue",
  protectedMode: true,
  source: $source
}' > /tmp/realtime-revenue-deploy.json

curl --fail-with-body \
  -H "Authorization: Bearer $GRID_TOKEN" \
  -H "Content-Type: application/json" \
  -X POST "$GRID_URL/api/models" \
  --data-binary @/tmp/realtime-revenue-deploy.json \
  | jq '{id, sourceHash, declaredInputs, declaredOutputs, diagnostics}'

Expected checkpoint on a compatible build: Revenue and TaxRate are declared inputs and NetRevenue is a declared output.

If that ID already exists, delete only this tutorial model with DELETE /api/models/realtime-revenue, then deploy again. Do not generalize cleanup to all models.

3. Connect with initial synchronization enabled

In terminal A, keep the default snapshot behavior:

curl --no-buffer --fail-with-body \
  -H "Authorization: Bearer $GRID_TOKEN" \
  "$GRID_URL/api/models/realtime-revenue/events"

Expected stream prefix:

  • stream:hello, with a server-owned stream ID;
  • model:snapshot, containing control-plane state;
  • a named heartbeat event every five seconds while the connection remains healthy.

The initial model snapshot is not a dump of every evaluated workbook value. After adopting it, explicitly resolve the symbols visible to the client.

4. Resolve the initial visible state

In terminal B:

curl --fail-with-body \
  -H "Authorization: Bearer $GRID_TOKEN" \
  -H "Content-Type: application/json" \
  -X POST "$GRID_URL/api/models/realtime-revenue/symbols/resolve" \
  --data '{"symbols":["Revenue","NetRevenue"]}' \
  | tee /tmp/realtime-initial.json \
  | jq '{
      revenue: .Revenue.value.value,
      netRevenue: .NetRevenue.value.value,
      requestRev: .Revenue.requestRev,
      resolvedRev: .NetRevenue.resolvedRev
    }'

Expected live checkpoint: Revenue = 100000 and NetRevenue = 79000. Record the server-owned revisions; do not manufacture or renumber them in the client.

5. Make one deliberate change and order it by revision

Still in terminal B, write the declared input:

curl --fail-with-body \
  -H "Authorization: Bearer $GRID_TOKEN" \
  -H "Content-Type: application/json" \
  -X PUT "$GRID_URL/api/models/realtime-revenue/input" \
  --data '{"symbol":"Revenue","value":120000}' \
  | tee /tmp/realtime-write.json \
  | jq '{symbol, status, value, requestRev, resolvedRev, lastError}'

Terminal A should receive a symbol:update for the new model revision. Resolve Revenue and NetRevenue again instead of assuming the event included every dependent value.

Expected checkpoint: Revenue = 120000 and NetRevenue = 94800.

For each symbol, accept a newly received snapshot only when its server-owned revision is not older than the snapshot your client already displays. The exact revision integers vary; their ordering is the contract.

6. Deliberately miss an event

In terminal A, stop curl with Ctrl-C. While disconnected, write another value in terminal B:

curl --fail-with-body \
  -H "Authorization: Bearer $GRID_TOKEN" \
  -H "Content-Type: application/json" \
  -X PUT "$GRID_URL/api/models/realtime-revenue/input" \
  --data '{"symbol":"Revenue","value":140000}' \
  | jq '{symbol, status, value, requestRev, resolvedRev, lastError}'

Reconnect in terminal A with the step 3 command. You should not expect the disconnected update to replay: this stream emits no SSE id field and has no Last-Event-ID replay contract.

Repair the stale client state by adopting the new initial snapshot and resolving the visible symbols again.

Expected repaired checkpoint: Revenue = 140000 and NetRevenue = 110600.

A client that reconnects with ?snapshot=false and does not refetch has no synchronization proof. Remove that parameter unless another explicit synchronization path exists.

7. Add the production reconnect policy

When a network error occurs or the named heartbeat stays silent beyond your chosen threshold:

  1. Close the silent connection.
  2. Reconnect with bounded exponential backoff and jitter.
  3. Keep the default initial snapshot enabled.
  4. Refetch visible symbols or visible ranges.
  5. Re-send viewport and presence state if the client uses them.
  6. Discard snapshots older than the revision already displayed for the same symbol.

Honor Retry-After if the reconnect path receives overload. Do not blindly retry mutations; read backpressure and retries.

8. Clean up

curl --fail-with-body \
  -H "Authorization: Bearer $GRID_TOKEN" \
  -X DELETE "$GRID_URL/api/models/realtime-revenue"

Expected response: true.

You are done when

  • The local transcript yields the four expected message classes.
  • The live connection begins with hello and model-snapshot control messages and continues with named heartbeats.
  • One write produces a newer revision and resolving the output returns 94,800.
  • You intentionally miss the 140,000 write, reconnect, and restore 110,600 by resynchronizing.
  • Your client does not claim Last-Event-ID replay or expose a bearer token in a browser URL.

Keep Realtime model events beside the implementation, and use Model lifecycle API for the resolve and input-write response shapes.

Build statusReached the expected checkpoint?