Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Realtime

Generally Available

Realtime streams row-level changes from your Data tables to connected clients. Subscribe to inserts, updates, and deletes on specific tables—or filter to rows matching a policy—without polling.

Realtime builds on the same Identity and policy model as Data. Every subscription is authenticated; the Rust policy engine decides which change events each subscriber receives. Transport is Server-Sent Events (SSE) first, with a WebSocket adapter for Supabase JS client compatibility.

Use Realtime for live dashboards, collaborative editors, notification feeds, and any UI that should reflect database state within milliseconds of a write.

  • Table subscriptions — Listen to all changes on a table or filter by column values.
  • Policy-aware delivery — Subscribers only receive events for rows they could read via Data API.
  • SSE with resume — Reconnect with Last-Event-ID to catch up after network blips.
  • WebSocket adapter — Drop-in compatibility layer for existing Supabase realtime clients.
  • Org-scoped channels — Active org from JWT + X-Reactor-Org scopes all subscriptions.
  • Low overhead — Built on Postgres logical decoding; no application-level polling.

Subscribe to new todos in your organization and log each insert.

Terminal window
reactor auth login user@example.com --password '...'
reactor realtime subscribe todos --events insert --org acme

Expected SSE event shape:

event: insert
data: {"table":"todos","schema":"public","new":{"id":"...","title":"Ship v1","done":false}}
event: heartbeat
data: {"ts":"2026-05-29T12:00:00Z"}

Only receive events when a column matches—useful for per-user or per-room feeds.

const channel = reactor.realtime
.channel('messages')
.on('insert', (payload) => appendMessage(payload.new))
.filter('room_id=eq.my-room-id');
await channel.subscribe();

Pass the last received event ID to avoid missing changes during reconnection.

let lastEventId: string | undefined;
const channel = reactor.realtime.channel('orders');
channel.on('*', (payload, meta) => {
lastEventId = meta.eventId;
handleChange(payload);
});
await channel.subscribe({ lastEventId });

Listen for inserts, updates, and deletes on the same table.

const channel = reactor.realtime.channel('todos');
channel
.on('insert', ({ new: row }) => addTodo(row))
.on('update', ({ old, new: row }) => updateTodo(old.id, row))
.on('delete', ({ old }) => removeTodo(old.id));
await channel.subscribe();
[data]
# Realtime shares the Data service and database connection
migrations_dir = "./migrations"
user_schema = "public"
[realtime]
enabled = true
heartbeat_interval_secs = 30
max_subscriptions_per_connection = 20
replay_buffer_secs = 300 # How long Last-Event-ID replay is available

Environment variables:

VariableDefaultDescription
REACTOR_DATA_DATABASE_URLSame Postgres as Data
REACTOR_REALTIME_ENABLEDtrueToggle realtime endpoints
REACTOR_REALTIME_HEARTBEAT_SECS30SSE keepalive interval
REACTOR_REALTIME_MAX_SUBSCRIPTIONS20Per-connection channel limit
REACTOR_REALTIME_REPLAY_BUFFER_SECS300Event replay window
LimitDefaultNotes
Subscriptions per connection20Additional channels require a new connection
Replay buffer5 minutesEvents older than this require a full resync
Max payload size64 KiBLarge row changes may be truncated with a fetch hint
Heartbeat interval30 secondsKeeps proxies from closing idle SSE connections
Concurrent connections per orgPlan-dependentContact support for high fan-out workloads
WebSocket fallbackSame limitsShares policy and quota enforcement with SSE

Event types:

EventPayload
insert{ new: Row }
update{ old: Row, new: Row }
delete{ old: Row }
heartbeat{ ts: ISO8601 }
  • HTTP base path: /data/v1/{table}/subscribe (SSE)
  • WebSocket path: /realtime/v1/websocket (Supabase-compatible protocol)
  • OpenAPI reference: Data API
  • JavaScript SDK: reactor.realtime
  • CLI: reactor realtime

Related capabilities:

  • Data — CRUD and policies that govern which events you receive
  • Identity — Authentication required for all subscriptions

Missing or expired JWT. Refresh the access token and reconnect. Realtime does not support anonymous subscriptions.

reactor.auth.onAuthStateChange(async (event) => {
if (event === 'TOKEN_REFRESHED') {
await channel.unsubscribe();
await channel.subscribe();
}
});
  1. Confirm the writing client uses the same org context (X-Reactor-Org)
  2. Check Data policies—the subscriber must have data:{table}:read on affected rows
  3. Verify Realtime is enabled: GET /data/v1/health should report realtime: ok

If Last-Event-ID is older than the replay buffer, perform a full Data query to resync state, then resubscribe without the header.

Some proxies buffer text/event-stream. Disable buffering for realtime paths or connect directly to the Reactor API origin. Heartbeats every 30 seconds help keep connections alive through most intermediaries.