Realtime
Push inserts, updates and deletes to your app as they happen, filtered by row-level security, plus broadcast and presence for live rooms.
Realtime pushes changes to your app the moment they happen, over one WebSocket. Subscribe to a table’s row changes, or open a room for broadcast messages and presence, such as typing indicators and who is online. Channels reconnect on their own.
Turn it on for a table#
On the database’s Data API tab, choose the table and switch on Realtime. The table must be in the Data API and have a primary key. Or do the same in SQL, with the secret key:
create table messages (
id bigint generated always as identity primary key,
room text not null,
author uuid not null default auth.uid(),
body text not null
);
alter table messages enable row level security;
create policy read_rooms on messages for select to authenticated using (true);
create policy write_own on messages for all to authenticated
using (author = auth.uid()) with check (author = auth.uid());
create trigger plexivo_realtime
after insert or update or delete on messages
for each row execute function auth.realtime_notify('id');From then on, every insert, update and delete on the table is sent, whether it comes through the Data API, GraphQL, raw SQL or your server’s own connection.
Row changes#
Subscribe to changes: and the table’s name, with an optional filter in the Data API’s syntax:
import { createClient } from '@plexivo/client';
const db = createClient(PLEXIVO_DATA_URL, PLEXIVO_PUBLISHABLE_KEY);
await db.auth.signUp({ email, password });
const feed = db.channel('changes:messages', { filter: 'room=eq.lobby' })
.onChange(({ type, record, old }) => console.log(type, record?.body ?? old.id))
.subscribe();
await feed.ready();
const message = await db.from('messages').create({ room: 'lobby', body: 'Hello, lobby' });
await db.from('messages').where('id', message.id).update({ body: 'Hello again' });
await db.from('messages').where('id', message.id).delete();import 'package:plexivo_client/plexivo_client.dart';
final db = PlexivoClient(dataUrl, publishableKey);
await db.signUp(email: email, password: password);
final feed = db.channel('changes:messages', 'room=eq.lobby')..subscribe();
feed.changes.listen((change) => print('${change.type} ${change.record?['body']}'));
await feed.ready();
await db.from('messages').create({'room': 'lobby', 'body': 'Hello from Flutter'});- An insert or update brings the new row as
record. A delete brings the deleted row’s primary key asold. - The filter is one condition, such as
room=eq.lobby,price=gt.100orstatus=in.(open,paid), with any operator of the Data API.
Only what each user may read#
Each subscriber receives a change only if their own row-level security policies let them read that row, and their filter matches it. For a delete, the old row is checked against their policies before its key is sent. Two users on the same channel can receive different changes, or none.
Rooms: broadcast and presence#
A room passes messages between the people in it; nothing is stored. Anyone with your publishable key can join room: rooms; room:private: rooms need sign-in.
const room = db.channel('room:lobby', { presence: { name: 'Asha' } })
.onPresence(({ event, state }) => console.log(event, Object.keys(state).length))
.subscribe();
const ben = createClient(PLEXIVO_DATA_URL, PLEXIVO_PUBLISHABLE_KEY, { persistSession: false });
const bensRoom = ben.channel('room:lobby')
.onBroadcast('typing', (payload) => console.log('typing', payload.name))
.subscribe();
await Promise.all([room.ready(), bensRoom.ready()]);
room.sendBroadcast('typing', { name: 'Asha' });final room = db.channel('room:lobby', null, {'name': 'Ravi'})..subscribe();
room.on('typing').listen((payload) => print('typing ${payload['name']}'));
room.presence.listen((p) => print(p.state.keys));
await room.ready();
room.sendBroadcast('typing', {'name': 'Ravi'});- A broadcast reaches everyone else in the room, not the sender. Its payload is a JSON object of up to 32 KB.
- Presence is what each member shares about themselves: up to 16 keys and 2 KB. Members get the full state on joining, then each change.
Staying connected#
A channel stays joined by itself. When the network drops, or the server ends the channel because the user’s access token has expired, the client refreshes the session if it needs to and joins again, with growing pauses between tries. Changes flow again as soon as it rejoins. Watch the status to show it:
feed.onStatus((status, error) => console.log(status)); // SUBSCRIBED, RECONNECTING, CHANNEL_ERROR, CLOSED
console.log(feed.state); // 'joined'feed.statusChanges.listen((status) => print(status)); // joined, reconnecting, …
print(feed.status);A join the server refuses, such as a table that is not in the Data API or a private room while signed out, is reported once and not retried:
await db.channel('changes:not_a_table').subscribe().ready();Clean up#
feed.unsubscribe();
room.unsubscribe();
bensRoom.unsubscribe();await feed.unsubscribe();
await room.unsubscribe();Next steps#
Updated 28 October 2026