Every chat app works on office Wi-Fi. The real test is the train, the elevator, and the hotel that charges for Wi-Fi by the hour.
"Make chat work offline" sounds like one feature. It's four, and only one of them is a cache. We just shipped offline support in CometChat's chat SDKs, so here's how each of the four plays out, including the one that's easiest to forget.
1. Open from the device
The obvious one. Store conversations and history locally, render them first, refresh from the server after. On a dead connection, users see their chats instead of a spinner.
The work hides in the word "store": how much, and what gets evicted when someone has hundreds of group chats. By default, CometChat keeps up to 100 conversations, 500 messages per conversation, and about 250 MB of content, evicting the oldest entries first. Every limit is configurable.
2. Hold what the user sends
A message typed in a tunnel has to survive the tunnel. That means a durable outbox, not an in-memory retry array, because users close apps constantly. Ours persists queued messages through an app restart (or a page reload on web) and sends them when the connection returns.
The subtle part is that queuing changes the contract of every send. A send used to end in sent or failed. Now there's a third state: pending. If an SDK starts queuing behind a screen that only knows sent-or-failed, the user sees an error for a message that's going to arrive anyway. So they send it again, the other person gets it twice, and you get a bug report nobody can reproduce on office Wi-Fi.
So in CometChat, a send only queues when you pass a listener that can hear about it:
CometChat.sendMessage(
textMessage,
new CometChat.MessageSentListener({
onMessagePending: (message) => {
// Queued and persisted; message.getStatus() === "PENDING"
},
onMessageSent: (message) => {
// Server ack: message now carries its real id and sentAt
},
onError: (error) => {
// FAILED_TO_FETCH while offline means "queued", not "failed"
},
})
).catch(() => {}); // the returned Promise also rejects while offline
No listener, no queue. Calls without one behave exactly as they did before, so you can adopt offline behavior one call at a time.
That last comment in the snippet surprises people. In the JavaScript SDK, an offline send still fires onError with FAILED_TO_FETCH, and the Promise rejects. It's a notice, not a failure: the message is still queued, and onMessageSent fires on reconnect. The Promise answers "did this go out just now?" while the listener tracks where the message actually is. To spot a real failure, check the status: PENDING sends on its own, FAILED waits for retryMessage().
3. Catch up on reconnect
When the network comes back, the SDK flushes the outbox and syncs the conversations and messages that changed while the device was away. Nobody has to pull to refresh.
Order matters here too. In the JavaScript SDK, a later send in the same conversation waits behind an earlier unsent one, so messages don't arrive shuffled.
4. Forget everything at logout
This is the one that's easiest to miss in a build-it-yourself version, and it's the one with privacy consequences. The cache belongs to a user, not a device.
Shared devices are everywhere: the tablet at a clinic front desk, the family iPad, the phone handed to a new shift. If the next person logs in and sees the last person's conversations because someone forgot a cleanup call, you have a much bigger problem than a stale cache.
In CometChat, logging out, a session expiring, or a different user logging in deletes the cache, unsent messages included. There's no cleanup call to forget.
What it doesn't do
In the JavaScript SDK, only new messages queue. Edits, deletes, and reactions still need a connection.
Encryption at rest is optional, and one more flag turns it on. It protects the cache on the device. It isn't end-to-end encryption.
Turning it on
Offline support is off by default. Enabling it is one setting at init:
const appSetting = new CometChat.AppSettingsBuilder()
.setRegion("REGION")
.setOfflineConfig({ enabled: true })
.build();
await CometChat.init("APP_ID", appSetting);
The same model ships in CometChat's JavaScript SDK 4.3.0+ (browser and Electron), Android SDK 5.1.0+, iOS SDK 4.2.0+, Flutter SDK 5.1.0+, and React Native Chat SDK 4.2.0+. The full rundown, with links to each SDK's docs, is on our blog: CometChat Offline Support.
Done right, your users never notice any of this. Neither does the code you shipped last year.

Top comments (0)