Realtime
Realtime pushes three kinds of events over one WebSocket per client:
- Database changes (
postgres_changes): inserts, updates and deletes, filtered by your RLS policies. - Broadcast: low-latency messages between clients, or from SQL after commit.
- Presence: who is online in a channel.
Enable
Section titled “Enable”Realtime streams only the tables you list. Use the dashboard (Realtime → Configuration) or the Management API:
PATCH /v1/projects/{ref}/config/realtime{ "enabled": true, "tables": ["public.todos", "public.messages"], "public_channels": ["lobby", "public:*"] }public_channels lists channels a client may join with private: false (an exact name or a prefix ending in *). Changes apply within about a second.
Database changes
Section titled “Database changes”const ch = base .channel("todos-feed") .on("postgres_changes", { event: "INSERT", schema: "public", table: "todos", filter: `user_id=eq.${userId}` }, (p) => { p.eventType // "INSERT" | "UPDATE" | "DELETE" p.new // new row p.old // DELETE: primary key only }) .on("system", {}, (p) => { if (p.code === "resync") refetch() }) .subscribe((status, err) => { // "SUBSCRIBED" | "CHANNEL_ERROR" (err.code) | "TIMED_OUT" | "CLOSED" })
await base.removeChannel(ch)A join is accepted when the table is enabled for realtime, the role has SELECT on it, a table with RLS has a single-column primary key and the filter column exists. Filters take one condition: eq, neq, lt, lte, gt, gte or in (status=in.(new,open)).
Every event is checked against your SELECT policy as the subscriber. Practical consequences:
- Use a filter such as
user_id=eq.<uid>whenever you can. At most 200 distinct groups may subscribe to a table without a filter; the next join fails withfilter_required. - A DELETE event carries only the primary key and goes to every unfiltered subscriber with SELECT on the table. If key values are sensitive, filter or use broadcast.
- Realtime is not a durable queue. After a reconnect (or a truncate or oversized transaction) you get a
systemevent withcode: "resync": refetch your data.
Broadcast
Section titled “Broadcast”const room = base.channel(`room:${roomId}`, { config: { broadcast: { self: false, ack: true } } }) .on("broadcast", { event: "typing" }, ({ payload }) => showTyping(payload)) .subscribe()
await room.send({ type: "broadcast", event: "typing", payload: { userId } }) // "ok" | "error" | "timed out"Private channels
Section titled “Private channels”Channels are private by default. A join is authorized by policies on realtime.channel_access, with action of join (receive), broadcast (send) or presence (track):
create policy room_members on realtime.channel_access for select to authenticatedusing ( channel like 'room:%' and exists (select 1 from public.room_members m where m.room_id = split_part(channel, ':', 2)::uuid and m.user_id = (select auth.uid())));A channel that only uses postgres_changes needs no channel policy. base.channel("lobby", { private: false }) works only for names listed in public_channels.
Presence
Section titled “Presence”const ch = base.channel(`room:${roomId}`, { config: { presence: { key: userId } } }) .on("presence", { event: "sync" }, () => render(ch.presenceState())) .on("presence", { event: "join" }, ({ key, newPresences }) => {}) .on("presence", { event: "leave" }, ({ key, leftPresences }) => {}) .subscribe()
await ch.track({ online: true, name }) // re-tracked after reconnectsPresence state is limited to 4 KiB per client.
Broadcast from the database
Section titled “Broadcast from the database”Triggers can publish events that are delivered only after the transaction commits:
create function public.notify_order_status() returns triggerlanguage plpgsql security definer set search_path = '' as $$begin perform realtime.send( channel => 'order:' || new.id, event => 'status_changed', payload => jsonb_build_object('status', new.status), private => true); return new;end $$;
create trigger order_status_broadcast after update of status on public.ordersfor each row execute function public.notify_order_status();realtime.send is not executable by anon or authenticated: call it from SECURITY DEFINER code. Payloads are limited to 64 KiB.
Connection behavior
Section titled “Connection behavior”The SDK keeps the access token out of the URL, refreshes it on the open socket, sends a heartbeat every 25 s and reconnects with exponential backoff (1 s up to 30 s). Channels become SUBSCRIBED again after each reconnect.
Default per-project limits: 500 connections, 20 connections per IP, 100 joins per connection, 20 client messages per second per connection, 64 KiB broadcast payload, 4 KiB presence state. Hitting a limit returns an error frame (rate_limited, too_many_joins, payload_too_large) and repeated violations close the socket. Connection and message allowances also depend on your plan.