# Electric Streams

The data primitive for the agent loop

```sh
npm i @durable-streams/client
```

[Quickstart](/docs/streams/quickstart) [Docs](/docs/streams)

## The agent loop is a stream of durable events

Every prompt, tool call, and generation is appended at a known **offset** on a persistent, real-time stream. Replay from any offset, branch off, or fan out to humans, agents, and [Electric Agents](/agents/) — over plain HTTP.

## Every stream is multiplayer

Streams aren't single-consumer. Any number of agents — and humans — can attach to the same stream, see each other's events as they land, and pick up exactly where they left off. The shared stream is the coordination layer.

[Read Durable Sessions for Collaborative AI →](/blog/2026/01/12/durable-sessions-for-collaborative-ai)

## The 30-second tour

Four curl commands. Create a stream, append a message, read it back, then tail it live.

```sh
# Create the stream
curl -X PUT http://localhost:4437/v1/stream/hello \
  -H 'Content-Type: application/json'

# Append one JSON message
curl -X POST http://localhost:4437/v1/stream/hello \
  -H 'Content-Type: application/json' \
  -d '{"hello":"world"}'

# Read from the start
curl "http://localhost:4437/v1/stream/hello?offset=-1"

# Tail live with SSE
curl -N "http://localhost:4437/v1/stream/hello?offset=-1&live=sse"
```

Run this yourself → [`/docs/streams/quickstart`](/docs/streams/quickstart)

## Ship your first durable stream in minutes

Install the client, create a stream, and start appending events. Subscribe live from anywhere on the network.

[Quickstart](/docs/streams/quickstart) [Docs](/docs/streams/)

## From the blog

Go deeper on Electric Streams, the Durable Streams protocol, and what teams are building on them.

- [Fork — branching for Durable Streams](/blog/2026/04/15/fork-branching-for-durable-streams)
- [Durable Streams — the data primitive for the agent loop](/blog/2026/04/08/data-primitive-agent-loop)
- [AI agents as CRDT peers — building collaborative AI with Yjs](/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs)
- [StreamDB — a reactive database in a Durable Stream](/blog/2026/03/26/stream-db)

[Electric Blog](/blog) [Follow @ElectricSQL](https://x.com/ElectricSQL)

## Streaming needs to be durable

SSE drops on a refresh. Tokens get lost on flaky networks. Resuming means re-running the request and re-billing the LLM.

Real apps need streams that **survive disconnects**, **persist across sessions**, and let many users and agents read and write the same conversation. That's what an **Electric Stream** is.

## Three properties that change everything

Electric Streams is a protocol, not a SaaS. The protocol is the product.

### URL-addressable

Every stream lives at its own URL. Works with curl, fetch, any load balancer, any CDN.

```
PUT   /v1/stream/hello
POST  /v1/stream/hello
GET   /v1/stream/hello
```

### Append-only

Once data is at an offset, it never changes. Offsets are opaque cursors that always sort in order.

```
POST  → 200 OK
      Stream-Next-Offset:
         01JQXK5V00
```

### Resumable

Reads return Stream-Next-Offset. Reconnect with ?offset=… and pick up exactly where you left off.

```
GET ?offset=01JQXK5V00
    → next chunk only
```

## Replay from any offset, exactly once

Producers identify themselves with three headers. Servers de-dupe. Clients resume from the last offset they saw. No external coordination required.

[Read the protocol →](/docs/streams/#producers)

## It's just HTTP — works everywhere

If your runtime can speak HTTP, it can read and write an Electric Stream. No SDK lock-in. No proprietary transport. No WebSocket infrastructure.

GET https://api.streams.dev/v1/stream/chat-42?live=sse

### TypeScript

Browser · Node · Workers

```
import { stream } from '@durable-streams/client'

for await (const m of stream({
  url: STREAM_URL, live: 'sse'
})) render(m)
```

### Python

Data scientists · Workers

```
from durable_streams import stream

with stream(STREAM_URL, live='sse') as r:
  for x in r.iter_json():
    process(x)
```

### Swift

iOS · macOS

```
let task = URLSession.shared
  .dataTask(with: URLRequest(url: URL(...)))
task.resume()
// SSE lines parsed by EventSource
```

### Go

Servers

```
resp, _ := http.Get(STREAM_URL)
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
  handle(scanner.Bytes())
}
```

### curl

Shell · Scripts · Debugging

```
curl -N "$URL?live=sse"

# prints SSE
# data: lines
```

## One protocol, four layers

Pick the layer you need. Bytes → JSON messages → typed CRUD events → reactive type-safe DB. Every layer above adds power; every layer below remains available.

### 1. Electric Streams

Append bytes, replay from any offset. The HTTP base protocol every other layer is built on.

```
PUT   /v1/stream/x
POST  /v1/stream/x
GET   /v1/stream/x?offset=…
```

[Concepts →](/docs/streams/)

### 2. JSON mode

Append JSON values, GET arrays. Message boundaries are preserved on the wire — no framing logic in your code.

```
Content-Type: application/json
POST  → {"hello":"world"}
GET   → [ {…}, {…}, {…} ]
```

[JSON mode →](/docs/streams/json-mode)

### 3. Durable State

Typed insert / update / delete events on the wire, plus snapshot markers. Materialise them into a live key-value view.

```
{"type":"user","value":{…},"headers":{"operation":"insert"}}
{"type":"user","key":"1","headers":{"operation":"delete"}}
{"headers":{"control":"snapshot-end"}}
```

[Durable State →](/docs/streams/durable-state)

### 4. StreamDB

Live, typed collections with queries and optimistic actions, layered on top of MaterializedState.

```
db.users.where({ … })
db.users.insert(row)
useLiveQuery(query)
```

[StreamDB →](/docs/streams/stream-db)

## Built on the open Durable Streams protocol

Electric Streams is one implementation. The protocol spec, conformance suite and reference clients live on [durablestreams.com](https://durablestreams.com/) — independent, fully open.

[durablestreams.com](https://durablestreams.com/) [Read the spec](https://github.com/durable-streams/durable-streams/blob/main/PROTOCOL.md)

## Built for the agent loop

From token streams to multi-agent collaboration — Electric Streams plugs into the AI stack you already use.

### TanStack AI

Durable connection adapter. Resumable, shareable AI sessions across tabs and devices.

[Docs](/docs/streams/integrations/tanstack-ai) · [Blog post](/blog/2026/01/12/durable-sessions-for-collaborative-ai)

### Vercel AI SDK

Durable Transport for the AI SDK. Drop-in replacement for streamText transport.

[Docs](/docs/streams/integrations/vercel-ai-sdk) · [Blog post](/blog/2026/03/24/durable-transport-ai-sdks)

## Your stack, not ours

Self-host the server with one binary, or run it on Electric Cloud. Producers and consumers are anything that speaks HTTP.

### producer.ts

```ts
import { DurableStream, IdempotentProducer } from "@durable-streams/client"

const handle = await DurableStream.create({
  url: STREAM_URL,
  contentType: "application/json",
})

const producer = new IdempotentProducer(
  handle, "llm-relay-1", { autoClaim: true }
)

for await (const chunk of llm.stream(prompt))
  producer.append(chunk)

await producer.flush()
```

### consumer.ts

```ts
import { stream } from "@durable-streams/client"

const res = await stream<ChatMessage>({
  url: STREAM_URL,
  offset: lastSeen ?? "-1",
  live: "sse",
})

res.subscribeJson(async (batch) => {
  for (const msg of batch.items) render(msg)
  lastSeen = batch.nextOffset
})
```

### curl.sh

```sh
curl -X POST $URL \
  -H 'Content-Type: application/json' \
  -d '{"event":"click"}'

curl -N "$URL?offset=-1&live=sse"
```

## Your first stream, end to end

Create a stream. Append a message. Subscribe live. Three steps, one package, real APIs.

stream.ts

```ts
import { DurableStream, stream } from "@durable-streams/client" // [1]

const url = "https://streams.example.com/v1/stream/chat"

const handle = await DurableStream.create({ // [2]
  url,
  contentType: "application/json", // [3]
})

await handle.append(JSON.stringify({ // [4]
  role: "user", text: "Hello"
}))

const res = await stream<{ role: string; text: string }>({
  url,
  offset: "-1", // [5]
  live: "sse", // [6]
})

res.subscribeJson(async (batch) => { // [7]
  for (const msg of batch.items) console.log(msg)
})
```

```sh
$ npx tsx stream.ts
✓ Created stream chat
✓ Appended message
→ { role: "user", text: "Hello" }
→ { role: "assistant", text: "Hi there!" }
→ ▍
```

1. **One package, two entry points** `DurableStream` for read/write handles. `stream()` for fetch-style consumption.
2. **DurableStream.create opens or creates** Idempotent: returns the existing handle if the stream already exists.
3. **Pick your content type** `application/json` enables [JSON mode](/docs/streams/json-mode) — message boundaries are preserved.
4. **Append messages** Each `append` is a single `POST`. Wrap with [`IdempotentProducer`](/docs/streams/clients/typescript#exactly-once-writes) for exactly-once delivery and batching.
5. **Resume from any offset** `"-1"` = beginning. Pass a saved offset to resume from exactly that point. `"now"` = skip the backlog.
6. **Live, in real time** `"sse"` opens a long-lived Server-Sent Events stream. `"long-poll"` works in environments that can't hold a connection open.
7. **Subscribe with batches** `subscribeJson` calls your handler with a `batch.items` array. The batch carries the next offset — save it to resume from later.

## Demos

Reference apps you can clone, run locally, and learn from.

- [Durable Doom](/streams/demos/durable-doom): Doom on Durable Streams. Live spectating, time-travel, and fork-to-continue — all client-side, no backend.
- [Collaborative AI Editor](/streams/demos/collaborative-ai-editor): Collaborative rich text editor where an AI agent is a server-side CRDT peer.
- [Territory Wars](/streams/demos/territory-wars): Multiplayer territory capture game built with Yjs CRDTs on Durable Streams.

[All demos](/streams/demos)

## Start streaming today

Create a stream, append events, subscribe live.

```sh
npm i @durable-streams/client
```

[Quickstart](/docs/streams/quickstart) [Docs](/docs/streams/) [GitHub](https://github.com/electric-sql/durable-streams)
