State changes.
Everyone follows.

Build realtime apps around shared state.
Define it once. Keep every client in sync.

Start building A TypeScript library for shared state
Play with shared statecounter://
Client AConnected
count00
Same definition. Same reducer.
Authoritative hub
count00
Committed state seq 000
Client BConnected
count00
Same definition. Same reducer.

Starting the shared counter…

Real library. In-memory transport. Simulated 220 ms latency.Click +1. Break the connection. Watch it catch up.
One shared definitionOptimistic by defaultServer-authoritativeReplay on reconnect

One definition.
Every side
of your app.

Your state and the actions that change it belong together. Write a pure reducer. Run it on the server and every connected client.

The hub commits the change.
Your clients follow the same logic.

Read the quick start

Start with the Deno workspace setup, then add this channel definition.

import * as v from "valibot";import { durableChannel, durableRoutes } from "durable_channel";export const counter = durableChannel()  .state(v.object({ count: v.number() }), { count: 0 })  .action((a) =>    a.name("counter/incremented")      .payload(v.object({ by: v.number() }))      .client()      .reduce((state, { by }) => ({        count: state.count + by,      }))  )  .build();export const routes = durableRoutes()  .route("counter://", counter).build();
A schema, an initial state, and the only way this counter can change.

Instant here.
Consistent everywhere.

Your interface responds immediately. Behind the scenes, the hub gives every committed action a place in the sequence.

ClientHubOther clients
Apply locally
dispatch(action)
Validate & persist
committed envelope
Run the same reducer

Local feels immediate.

The client applies your action optimistically, then reconciles with the hub’s response. If the hub rejects a change, the optimistic update rolls back.

The hub has the final word.

Validate the payload. Run the reducer. Persist the state. Broadcast the envelope. Each commit happens in order.

A dropped connection isn’t the end.

Reconnect to replay missed actions. If the gap exceeds the replay window, a fresh snapshot brings the client up to date. Your app decides when to reconnect.

Try disconnecting a client

Give every change
a clear purpose.

A small vocabulary for the moving parts
of your realtime application.

State
The data at a URI, with a schema and an initial value.
.state()
Actions
Named changes, reduced on the hub and mirrored by clients.
.action()
Commands
Server-side work with typed parameters and results.
.command()
Notifications
Ephemeral messages. Delivered live, never persisted or replayed.
.notification()

Your next idea.
Already in sync.

Start with a channel. Build from there.

Build your first channel