Skip to main content
Run a consumer that persists both items and its cursor in SQLite. SSE is the default for a long-running service; polling uses the same transaction and saved position. These examples follow the dashboard consumer’s serial apply and explicit reconnect contract, with concrete storage and transport recovery.

Node.js: SSE and polling

Use Node.js 22.13 or newer. Save the following as consumer.mjs. It uses built-in fetch and SQLite, so you do not need a package install. Keep STREAM_ID fixed for one chain/source/filter; use a different ID when any of those change.
Run one process per STREAM_ID. Stop the SSE process before trying polling against the same database. Both modes resume from the committed cursor.
Unlike EventSource’s automatic reconnect, this loop uses only the checkpoint that SQLite committed. It discards partial pages, honors HTTP Retry-After, handles SSE error controls, and closes after 60 seconds without bytes. A 409 clears only this source and rebuilds from START_BLOCK. Apply failures stop the process. Add derived model updates inside apply’s transaction.

Python: polling

Save this as streams.py. It uses only Python’s standard library and SQLite. Use the same environment variables as above; keep START_BLOCK numeric for a complete rebuild. Run python3 streams.py for polling.

Python: SSE

Save this beside streams.py as streams_sse.py and run python3 streams_sse.py. It reuses the same database transaction and checkpoint. Do not run it alongside the polling consumer for the same source.

Side effects at the tip

Treat data near the tip as provisional. Replacing database rows can undo a reorg; it cannot undo an email, a payout or a webhook already sent. Use a finalized outbox for irreversible effects:
  1. In the page transaction, replace source rows from page.from, replace pending outbox rows in that range, insert new items and pending effects, and save next.
  2. In that transaction, persist the highest finalized.number seen for this source. Keep it across lower or null marks and process restarts. Do not substitute safe or tip for finality.
  3. A separate worker sends pending effects whose block is at or below the stored finalized mark. Give each effect a stable idempotency key, such as source, transaction hash, log index and effect type.
  4. Record delivery. A crash after sending but before recording can send again, so the receiver must deduplicate using that key.
Keep delivered-effect deduplication records across a resync rebuild. Reconstruct pending effects from the rebuilt source without repeating delivered ones. These consumers store raw items only; add the outbox in your app’s transaction.

Limits

Use a durable local database volume. Keep source identities separate and serialize writers for each source. The server does not track your cursor, so replaying from an older position bills those pages again.

Next