Live queries & change feed
In-process real-time subscriptions over generated WebSocket endpoints — a field-blind change feed and stateful, removal-aware live queries — with honest notes on single-process scope and the O(rows) re-run cost.
ForgeDB generates two real-time WebSocket surfaces over your schema: a change feed that pushes a typed event as each row changes, and live queries that maintain a filtered result set and emit add/update/remove deltas. Both run in-process and are best-effort. Both bind to the same generated filter as the REST list endpoint, so there is no second predicate parser to keep in sync.
Change feed — /subscribe/<model>#
Subscribe to a model to get a typed JSON event on every insert, update, and delete. A query string filters the stream by the same fields the REST list endpoint accepts.
# Subscribe to filtered inserts/updates/deletes for a model:
wscat -c "ws://localhost:8080/subscribe/post?status=published"Events carry ChangeKind::{Inserted, Updated, Deleted, Linked}. A Deleted event
includes the pre-delete row, so the record is still materializable. Filters compare by
typed value, not by string — ?views=3 matches a stored 3.0.
Every mutation emits a field-blind signal (model, row_index, kind) onto an
in-process broadcast. The schema-agnostic forgedb-changefeed substrate carries the
model name, never a field. The generated WebSocket endpoint routes by model name,
materializes the row through the read path, applies a generated per-model filter, and
streams the typed JSON.
Because the comparison is generated once per field, bool, uuid, decimal, timestamp, and
enum all compare by value. The earlier serde_json stringify fragility is gone.
Live queries — /live-query/<model>#
A live query is stateful and removal-aware. It sends the current matching set as an
Init delta, then streams Added / Updated / Removed deltas as rows enter, change,
or leave the set.
wscat -c "ws://localhost:8080/live-query/post?status=published"The query-string params bind to the same generated closed-set filter as the REST list endpoint. On each coarse change signal the handler re-runs that filtered query and diffs the new result against the last one by id, emitting a delta per changed row.
Removed is expressible because a delete appends a tombstoned version rather than erasing
bytes, so the disappearance is observable. Updated is detected by a generated typed
per-field comparison, not a string hash.
One filter, everywhere
The change feed, live queries, and the REST list endpoint all bind to the same generated
per-model closed-set matcher (<model>_event_matches). There is no second, drift-prone
predicate parser — the field-aware logic is generated once.
Limits#
- Single-process. Both surfaces are in-process, best-effort broadcasts — there is no cross-process broker on this path. For a durable, resumable, cross-process stream (which feeds the browser read-replica), see the replication broker in Browser read-replica.
- Live queries re-run O(rows) per matched event per connection, with no coalescing/debounce. A selective filter re-runs a narrow scan and materializes only the matching rows, but a broad filter materializes its whole matching set. This is the scaling cliff to keep in mind for high-throughput or broad subscriptions.
- The filter is exact-match (
=), not range or prefix — for value ranges use an ordered index.