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.

Walk down the creekGet started
The StreamOtter otter swimming through a stream of code
Connecting to the field station…Lontra Creek field station
field instruments → Kafka → gateway
Your browserconnection connecting
LC-02Kestrel Bend gaugeidle
Flow
— cfs
Water
— °C
Oxygen
— mg/L
—waiting
LO-07Pebble, adult femaleidle
Where
—
Doing
—
Dives/hr
—

—waiting
LC-03Slate Canyon gaugeidle
Stage
— ft
Turbidity
— NTU
Trend
—
—waiting

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 dev

    Then 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.

    MIT licensedNode.js 24 or laterSocket.IO 4.8 transportTested with Apache Kafka 4.1.20.1.0-rc.3 on npm

    How 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.

    1. Describe the state

      A channel names one piece of state, its parameters, and its schema. The gateway checks every payload against it, and streamotter generate turns it into TypeScript types.

      {
        "station": {
          "delivery": {
            "kind": "state",
            "overflow": "resync"
          },
          "handlersRef": "station",
          "paramsSchema": "StationParams",
          "payloadSchema": "GaugeReading",
          "source": "field",
          "version": 1
        }
      }
    2. 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)
      };
    3. 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.

    authorizing

    Your handler is deciding whether this viewer may have this channel.

    synchronizing

    Loading a snapshot. Updates that arrive meanwhile are held, then released in revision order.

    live

    Caught up, with a healthy source. Show it as current.

    stale

    The connection or the source dropped. Keep showing the last state, marked stale, while StreamOtter recovers.

    resync-required

    Automatic recovery stopped after three tries. Offer a retry; resync() starts over.

    failed

    Refused or broken for good, like FORBIDDEN or invalid data. Replace the subscription.

    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.

    Read the getting-started guide

    npm init -y
    npm install streamotter
    npx streamotter init .
    npx streamotter dev --config streamotter.json --handlers server/handlers.mjs