Feeds — Live evolution
A feed page is a snapshot. Live evolution is the protocol that tells the UI when that snapshot is stale: "there are N new things that would land in this feed if you refetched."
The mechanism is a debounced WebSocket tick. No row payloads cross the wire
— just counts and a topChanged hint. The UI decides what to do with them.
The tick contract
Frequency is throttled by evolve.minNotifyInterval on the feed config
(default '30s'). The server coalesces multiple NOTIFYs within the window
into one tick.
Configuring evolution
pollOnIngest: true(default) — the server listens for row ingest on the lens and emits ticks when new candidates match the feed's predicates.pollOnIngest: false— no automatic ticks. Useful for feeds where freshness comes from a separate signal (e.g. you compute ranks on a cron).
onSubscribe modes 'replace' and 'merge' are declared in the config
(for forward-compat with client behavior), but the v1 tick contract is
"count-only." The UI decides whether to refetch, prepend, or ignore. Modes
'replace' and 'merge' become meaningful when the Beam codegen ships
stronger React primitives in a future release — safe to configure now.
Subscribe from the client
The iterator is long-lived — awaits the next tick each time through the
loop. break or an unhandled throw closes the WebSocket op (the shared
connection stays open for other ops).
React hook
useFeed owns the subscription lifecycle for you:
liveCount— running sum ofnewCountsince the lastrefetch().evolved— mirrors theFeedPage.evolvedflag on the most recent page.evolveOn— when to auto-refetch:'topChanged'(default),'any','never'.
Access rules for streaming
grants.stream is separate from grants.feed:
A lens can expose HTTP feed pages without streaming, or vice-versa. Feeds
with liveUpdates: true require 'live' in stream.modes.
The raw WebSocket op
If you're writing a non-TypeScript client, here's the frame protocol:
Close codes and the full frame protocol live in HTTP & WebSocket.
What ticks don't carry
Ticks are signals, not payloads. They tell the UI "something changed" —
the UI decides whether to care. If you want the new rows themselves, call
beam.<lens>.feed.<name>() again (or useFeed's refetch()).
This keeps the WS protocol cheap: 100k subscribers × one small tick every 30s is a fraction of the bandwidth of streaming payloads to each. And the same approach works whether your feed returns 10 items or 10,000 — ticks don't scale with result size.