Real-time
that survives a load balancer.
Reactivity isn’t a socket you manage — it’s the default. The runtime streams changes over one WebSocket as id-keyed delta patches, driven by native database change-data-capture, and fans them across every replica so a write on one instance surfaces on all of them.
// posts.list.query.ts → source drives invalidation
export const listPosts = defineQuery({
name: 'posts.list', source: 'posts',
input: Schema.Struct({}), output: Schema.Array(Post),
})
// In any component — one hook, live forever
const { data } = useSubscription('app', 'posts.list')
// A write on ANY replica pushes a delta here.
// No polling, no refetch, no useEffect.How live data actually travels.
Snapshot + delta wire protocol
One WebSocket carries every subscription. The first event is a full snapshot; the rest are id-keyed RFC-6902-style delta patches with an order array. Pure reorders emit zero ops, no-ops are suppressed, and per-subscription errors are isolated so one bad query can’t stall its siblings. A slow consumer never grows the server: while its socket is blocked, updates coalesce onto the newest state (bounded memory per subscription), and a persistently blocked one is closed loudly with a typed overrun error — never dropped silently.
Native change-data-capture
Changes come from the database itself: Postgres LISTEN/NOTIFY, MySQL/MariaDB binlog, MSSQL Change Tracking, or an in-process bus for SQLite — so a write from anywhere (even a raw SQL client) wakes the right subscriptions.
Cross-replica fan-out
@voltro/plugin-broadcast fans app mutations to every instance behind a load balancer over Redis, NATS or memory. It’s additive to inline emit, so local reactivity survives a broker outage — and it closes the single-instance gap for SQLite and MSSQL.
Presence & typing
@voltro/plugin-presence tracks ephemeral per-channel presence with a heartbeat roster and typing indicators (usePresence / useTyping), held in memory and owner-partitioned — no table is written. Add plugin-broadcast and every replica sees the whole room; without a broker each replica sees only the clients connected to it, which the boot warns about because a correct roster of a fraction of the room looks exactly like success.
Auto-optimistic, for free
Optimistic cache patches are derived from a mutation’s target metadata (its table + op) — override with .withOptimistic() or turn it off with .withoutOptimistic(). The list updates before the round-trip completes, then reconciles.
One-shot reads share the path
The same JSON envelope powers a POST /rpc one-shot path that drains a query to its first snapshot — exactly what SSR loaders use for first paint, with the same auth middleware. And for foreign protocols — a Yjs provider, a legacy device fleet — a raw WebSocket gateway (defineWebSocket in a *.ws.ts file) mounts its own upgrade path beside the rpc socket: subject-authenticated before the upgrade, origin-guarded against cross-site hijacking, bound to the credential's expiry.
When no tab is open, web push
The socket only reaches an open tab. For the rest, plugin-notifications adds a first-party web-push channel: payloads encrypted per RFC 8291, VAPID auth per RFC 8292, subscriptions held per subject AND per browser endpoint, and an endpoint the push service answers 404/410 for is pruned automatically. One secret — VOLTRO_VAPID_PRIVATE_KEY, minted per project by voltro dev — and the service worker ships with the package.
Reactivity that scales out.
A write anywhere, seen everywhere
On Postgres and MySQL/MariaDB the fan-out is native — a write on any replica surfaces on every replica. For dialects without native cross-instance CDC, or any deployment behind a load balancer, plugin-broadcast adds a Redis or NATS bus on top of the inline emit, so scaling from one instance to a fleet doesn’t change a line of your application code.
How a change reaches a hundred browsers.
- 1
A write lands in the database
It does not matter which replica took it, and it does not matter whether a person, a workflow or a scheduled job made it.
- 2
The database announces it
Postgres and MySQL publish their own change feed, so there is no polling loop and no second source of truth that can lag the first.
- 3
Every process hears it
The fan-out is fleet-wide rather than per process — a browser connected to replica three sees a write that landed on replica one.
- 4
Only the affected screens re-render
A subscription declared the table it reads, so the match is a declaration rather than a topic string two places have to spell the same way.
Real-time fan-out, in depth.
Why do most real-time features break when you add a second server?
Because they are built on in-process events. A mutation handled by instance A emits an event that only instance A's sockets hear, so a user connected to instance B sees nothing until they reload. It works perfectly in development, where there is one process, and fails the day a load balancer appears.
Voltro takes the change from the database instead of from the process that made it. Postgres LISTEN/NOTIFY, the MySQL and MariaDB binlog and SQL Server Change Tracking are fleet-wide by construction: every instance hears about every committed write, whichever instance performed it.
That also covers the writes your application did not make. A migration, an admin running SQL by hand, a nightly job in another service — all of them produce the same change events, so the UI is correct rather than correct-only-when-changes-came-through-the-app.
What exactly travels over the socket?
One WebSocket per client carries every subscription it holds. The first message for a subscription is a full snapshot; after that the server sends id-keyed delta patches plus an ordering array, so the cost of a change is the changed rows rather than the whole result set.
The details are where a live UI is won or lost: a pure reorder emits no operations at all, a computed no-op is suppressed rather than sent, and an error in one subscription is isolated so a single failing query cannot stall the others sharing the socket.
Presence rides the same idea but deliberately does not touch the database. A heartbeat roster per channel lives in memory, owner-partitioned, so "who is looking at this record" costs no writes — and with the broadcast plugin every replica sees the whole room instead of only its own clients.
What do I add when my dialect has no fleet-wide feed?
A broadcast plugin, pointed at Redis or NATS. It fans application change events to every instance, which closes the gap for SQLite and for any deployment where you would rather have an explicit bus than rely on database notifications.
It is additive to the in-process emit rather than a replacement, so local reactivity keeps working if the broker is briefly unreachable — a broker outage degrades cross-instance freshness instead of breaking the page.
Nothing in your application code changes when you add it. The subscription API, the hooks and the query declarations are identical; scaling from one container to twenty is a plugin and an environment variable.
How does this interact with authorization and tenancy?
Fan-out is tenant-scoped: a subscriber only wakes for writes inside its own tenant, so a busy tenant does not push work onto every other tenant's sockets — and cannot leak the existence of its rows through timing.
Subscriptions carry the same access decision as any other procedure. A query must declare who may call it, and that decision is enforced when the subscription opens rather than only on the first HTTP fetch, which is the gap a bolted-on socket layer usually leaves.
Row-level visibility is resolved before the executor runs, so a change to a row a subscriber may not see produces no delta for that subscriber. Filtering after the fact would mean the server had already decided to send it.
The pieces that make it survive scale.
| Component | Responsibility |
|---|---|
| Change capture | Native per dialect — LISTEN/NOTIFY, binlog, Change Tracking, or an in-process bus for SQLite. |
| Delta protocol | Snapshot then id-keyed patches with an order array; no-ops suppressed, pure reorders free. |
| Cross-replica bus | Redis or NATS via the broadcast plugin — additive, so a broker outage degrades rather than breaks. |
| Presence | In-memory heartbeat roster per channel; no table written, cross-instance with the bus. |
| Tenancy | Fan-out scoped per tenant; row visibility resolved before a delta is produced. |
| First paint | A one-shot path drains a subscription to its first snapshot for server-rendered loaders. |
| Comment threads | plugin-comments rides the same reactive channel — a new comment reaches every open thread as a delta, with no second push mechanism. |
Frequently asked questions
Do I need Redis for real-time?
Not on Postgres or MySQL/MariaDB, where the database's own change feed is already fleet-wide. You add Redis or NATS when your dialect has no cross-instance feed — SQLite — or when you want an explicit bus rather than depending on database notifications.
Does a write from outside the app reach subscribers?
Yes. Because changes come from the database rather than from application events, a migration, a manual SQL statement or another service writing to the same tables all wake the right subscriptions.
What is the cost of a large live list?
Proportional to what changed, not to the list. Updates are id-keyed deltas, pure reorders emit nothing, and there is a windowed subscription for lists long enough that you only want a slice in memory.
Is presence stored in the database?
No, deliberately. The roster is in memory and owner-partitioned, so ephemeral state does not become write traffic. Without a broker each replica sees only its own clients, which the boot warns about — a correct roster of a fraction of the room looks exactly like success.
How are subscriptions authorized?
By the same declaration every procedure carries, enforced when the subscription opens rather than only on an initial fetch. Row visibility is resolved before deltas are produced, so a subscriber never receives a change for a row it may not see.
Real-time builds on the data layer.
Open the framework. See it for yourself.
Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.