Realtime on a phone
Holding a websocket across backgrounding, network changes and reconnection, without replaying the whole collection.
Basket pushes changes over a websocket, which is the difference between a list that updates and a list with a refresh button. The protocol itself is on the realtime reference. This page is about the part a phone makes hard: the connection will not stay up, and you have to be fine with that.
What a mobile connection actually does
app opens connect, subscribe, receive snapshot
user switches app OS suspends the process; the socket dies quietly
user returns reconnect, resume from cursor, receive only what changed
train, lift, tube socket drops; reconnect with backoff
wifi → cellular the socket is gone even though "the internet works"
None of these are errors. They are the normal life of a mobile connection, and the protocol has one feature that makes them cheap to handle.
The cursor is the whole trick
Every event carries a cursor. Keep the last one you processed. When you reconnect,
send it back with snapshot: false and the stream resumes exactly where it stopped:
// first connection of the session
{ "type": "subscribe", "project": "prj_…", "key": "basket_public_…",
"collection": "messages", "userToken": "eyJ…",
"where": [["room_id", "==", "r_42"]] }
// every reconnection after that
{ "type": "subscribe", "project": "prj_…", "key": "basket_public_…",
"collection": "messages", "userToken": "eyJ…",
"where": [["room_id", "==", "r_42"]],
"cursor": "eyJ…", "snapshot": false }
Reconnecting like a good citizen
Three rules, in order of how much trouble ignoring them causes:
- Back off exponentially, with jitter. Start around a second, double up to roughly thirty, and add a random fraction. Without jitter, every device that lost connection to the same tower reconnects in the same instant.
- Only reconnect when there is a network. Listen to
NWPathMonitoron iOS orConnectivityManageron Android and reconnect when the path comes back, instead of retrying into a void and burning battery. - Reset the backoff after a successful subscribe — not merely after the socket opens. A socket that opens and is then refused is not a success.
Backgrounding
Neither platform lets an ordinary app hold a socket open indefinitely in the background. Do not fight it:
- Close the connection when you go to the background, deliberately, and keep the cursor. A socket you closed is one the system will not kill in a way you have to detect.
- Reconnect on foreground with the stored cursor. If the app was away long enough that the access token expired, refresh (carefully) before subscribing.
- Do not use realtime for notifications. If something must reach the user while your app is not running, that is APNs or FCM, sent from a Function. A websocket cannot wake an app the OS has suspended.
Rules apply to the stream too
A subscription is subject to the same collection rules as a query. Passing
userToken makes the signed-in user available to those rules, so a subscriber receives
only the records they were allowed to read in the first place. Leaving it out subscribes
anonymously, and an anonymous subscriber sees whatever your rules permit an anonymous reader to
see — which should usually be nothing.
The token in a long-lived subscription is checked when you subscribe. Resubscribe after a refresh rather than assuming an old connection carries the new session.
Merging events into what is on screen
Events arrive as created, updated and deleted. Apply them to your local store, and let your UI observe that store rather than the socket — one path into the list, whether the change came from the network or from the person using the app.
// Kotlin — the socket writes to the store; the UI observes the store
when (event.type) {
"created", "updated" -> dao.upsert(event.record.toEntity())
"deleted" -> dao.deleteById(event.record.id)
}
prefs.cursor = event.cursor // persist AFTER applying, never before
Persist the cursor after you have applied the change, not when it arrives. If the process dies between the two, you want the event replayed rather than skipped — arriving twice is harmless if your writes are upserts keyed by record id, and a gap is not.
Optimistic updates
A write goes over HTTPS and comes back over the websocket. Show the change immediately, mark it pending, and let the event confirm it — matching on the record id, which is the same in both. If the write fails, roll the local change back; if the event never arrives but the write succeeded, your next reconnect will bring it with the cursor.
What it costs
Realtime messages are metered, and a snapshot counts as reads. Two habits keep that small: always
reconnect with a cursor, and always subscribe with a where clause narrow enough that
you are not receiving a whole collection to display twenty rows.
Billing →
Next
- Realtime reference — every frame, field and error.
- Queries — the same filters your subscription uses.
- Building a mobile app — back to the overview.