- Flow
- — cfs
- Water
- — °C
- Oxygen
- — mg/L
StreamOtter 0.1.0-rc.3 · MIT · Kafka to the browser
Live state from Kafka to the browser. Never silently wrong.
StreamOtter is a gateway and a TypeScript SDK for state that changes. Each view starts from a snapshot, updates in revision order, and tells you when it's stale. Your own code decides who may see what.

- Where
- —
- Doing
- —
- Dives/hr
- —
- Stage
- — ft
- Turbidity
- — NTU
- Trend
- —
Try it: cut this page's connection. The creek keeps moving without you, and every view says it's stale until fresh snapshots arrive.
The live demo isn't answering right now. The field station's server may be restarting or down; the rest of this site works without it. You can run the same demo on your machine with Node.js 24 or later, no Kafka needed:
git clone https://github.com/jfricano/lontra-creek.git
cd lontra-creek
npm install
npm run devThen open http://127.0.0.1:4321. The demo's source is on GitHub.
A real StreamOtter subscription from this page to the Lontra Creek field station. Lontra Creek is a made-up river-otter study: the otters and the weather are simulated, and the data pipeline is real.
0.1.0-rc.3 on npmHow it fits together
Describe the state, protect it, subscribe to it
Browsers subscribe to state channels, not raw topics. You own identity, access rules, and the source of truth; StreamOtter owns synchronization, ordering, delivery, and recovery.
Describe the state
A channel names one piece of state, its parameters, and its schema. The gateway checks every payload against it, and
streamotter generateturns it into TypeScript types.{ "station": { "delivery": { "kind": "state", "overflow": "resync" }, "handlersRef": "station", "paramsSchema": "StationParams", "payloadSchema": "GaugeReading", "source": "field", "version": 1 } }Decide who sees it
Your handlers authenticate viewers, authorize each channel, pick state out of Kafka records, and read snapshots from your own store. Access is never guessed.
export const holt: HoltHandlers = { // Den sites are protected: only researchers may subscribe. authorize: ({ principal }) => principal.claims["role"] === "researcher", // Pick this channel's state out of each Kafka record. map: ({ record }) => holtStates(record.value), // The current state, read before any buffered update is released. snapshot: ({ params, signal }) => fieldStation.holt(params.holtId, signal) };Subscribe in the browser
The SDK hands you a snapshot, then every newer state in order, plus a state you can show. After a disconnect it resynchronizes from a fresh snapshot on its own.
const client = createClient<AppChannels>({ getToken: ({ signal }) => session.token(signal) }); const gauge = client.subscribe("station", { channelVersion: 1, params: { stationId: "LC-02" } }); gauge.on("data", event => render(event.data)); // a snapshot, then newer states in order gauge.on("state", ({ state }) => showState(state)); // live, stale, resync-required… await gauge.ready();
No silent failures
Every view is in a state you can name
A view is never reported as live after a disconnect, a restart, or a slow connection. When something goes wrong, the state says what, and the error says what to do.
Your handler is deciding whether this viewer may have this channel.
Loading a snapshot. Updates that arrive meanwhile are held, then released in revision order.
Caught up, with a healthy source. Show it as current.
The connection or the source dropped. Keep showing the last state, marked stale, while StreamOtter recovers.
Automatic recovery stopped after three tries. Offer a retry; resync() starts over.
Refused or broken for good, like FORBIDDEN or invalid data. Replace the subscription.
At the creek
See it work, then see it break
Five minutes, no Kafka needed
Try it on your machine
The scaffold uses a built-in fixture source. Open the workbench URL that dev prints, preview a channel, and advance the fixture to watch revisions arrive.
npm init -y
npm install streamotter
npx streamotter init .
npx streamotter dev --config streamotter.json --handlers server/handlers.mjs