No lost messages: clients that reconnect get the broadcasts they missed, in order, with no duplicates.
Phones go through tunnels, laptops sleep, tabs lose their connection. With message history, a channel that reconnects first receives the broadcasts it missed, in the order they were sent, then carries on live. Your listeners get each message once.
#Turn it on
In Studio open the Realtime inspector, choose Message history and Turn on message history. Then create channels with history: true:
const chat = stitchdb.channel('chat:42', { config: { private: true, history: true } })
chat
.on('broadcast', { event: 'message' }, ({ payload, meta }) => {
// meta.replayed is true for messages delivered after a reconnect
showMessage(payload)
})
.subscribe()
// Send a message that is kept for clients that are offline right now.
// Resolves once it is stored: that is your delivery acknowledgement.
await chat.publish('message', { text: 'On my way' })To also get recent messages on the first join (for example to show the last minute of a chat), pass history: { since: Date.now() - 60_000 }.
#Which messages are kept
| Sent with | Kept |
|---|---|
channel.publish(event, payload) | Yes |
realtime.send() in SQL, triggers, Edge Functions | Yes, see Broadcast from the database |
channel.send() over the socket | No: fast and ephemeral (cursors, typing indicators). Use ack: true to know the server got it. |
| Postgres changes | No: send them with realtime.send() from a trigger to make them replayable |
| Presence | Not needed: the current state is sent on every join |
Messages are kept for a few minutes, depending on your plan:
| Plan | History |
|---|---|
| Free | 2 minutes |
| Pro | 10 minutes |
| Scale | 30 minutes |
| Enterprise | 60 minutes |
A client that was away longer gets everything still kept, and should reload its data from the database for the rest.
#Who can read and send
Public channels: anyone with your project key, as on the socket. Private channels (private: true): your Realtime policies on realtime.messages decide, for history exactly as for live messages. A read policy lets a user receive and replay; an insert policy lets them publish().
-- Members of a chat room read and send its messages (topics like chat:<room id>)
create policy "room members read" on realtime.messages for select to authenticated
using (exists (select 1 from public.room_members m where m.user_id = auth.uid() and 'chat:' || m.room_id = realtime.topic()));
create policy "room members send" on realtime.messages for insert to authenticated
with check (exists (select 1 from public.room_members m where m.user_id = auth.uid() and 'chat:' || m.room_id = realtime.topic()));#HTTP API
Other languages can use the same endpoint the SDK uses, with the apikey header and, for a signed-in user, Authorization: Bearer <access token>:
| Request | Does |
|---|---|
GET /history/v1/messages?topic=chat:42&after=<message id> | Messages after that one, oldest first (since=<ms> instead of after, limit up to 500, private=false for public channels). Returns messages, more and gap (true if some messages were older than the history window). |
POST /history/v1/messages with { topic, event, payload, private } | Stores and broadcasts a message; returns its id |
Sends count toward your Realtime messages and are limited to your plan's messages per second.