Building a Real-Time Calls Feature on CometChat: What Actually Hurts
Two React Native apps, one CometChat SDK, and a long list of edge cases. Here's what we learned shipping live consultations between users and astrologers.
The first time someone asked me how the Calls feature works, I gave them a 30-second answer and we both moved on. The real answer takes about three weeks of pain to appreciate.
This post is the long version.
The Setup
Mynaksh runs as two React Native apps: one for users, one for astrologers. They are separate apps, separate codebases, separate App Store / Play Store listings. They share design language and a backend, and that's about it.
When a user "calls an astrologer," what actually happens is: a session needs to be set up across two app instances, on two different devices, possibly on two different networks, possibly on two different OS versions, and the audio has to feel as good as a regular phone call from the moment they pick up.
Why We Used CometChat
The honest answer to "why CometChat" is: we did not want to build real-time messaging and calls infrastructure from scratch. Building voice/video on top of WebRTC is a multi-quarter project for a team that has done it before. We hadn't.
CometChat gives us:
- Voice and video calls
- Messaging (chat sessions are a real product surface for us — astrologers and users often message before or after calls)
- Presence (online/offline state, typing indicators)
- Push notifications integration
What it doesn't give us is anything specific to our product. The hard work was on top of the SDK.
The Two-App Problem
If you've only ever built one app, the dual-app architecture has surprises.
The first time we tested a real cross-app call, on real devices, on real networks, the user app rang and the astrologer app... didn't. The FCM notification just never arrived. We spent two days walking through the obvious things — APNS/FCM tokens, payload shape, server keys — before someone tried the test on a different physical Android phone and it worked first try. The original device was a Xiaomi running MIUI's "Battery Saver" mode, which silently kills FCM delivery for backgrounded apps. That's now a documented requirement in our QA setup: every Android test device gets battery optimization disabled for both apps, on day one.
A few things we had to build on top of CometChat to make the two apps feel like one continuous product:
- Custom signaling layered on CometChat events for "session started," "session ended," "session ended unexpectedly" — because we need our own state machine, separate from CometChat's.
- Reservation flow that locks an astrologer to a user before the call so two users don't end up calling the same astrologer at the same moment.
- Wallet integration tied to call duration. The Calls feature and the wallet system are deeply coupled — see the wallet post for that side.
React Native Realities
React Native in 2026 is fine. It is also still React Native.
The CometChat SDK is a wrapper around native modules. Most of the time it works. The interesting failures show up at the OS edges. Three concrete things we had to dip into native modules for:
- Background audio on Xiaomi/Oppo/Vivo handsets. The OEM skins on those Android forks have aggressive background-process killing. CometChat's default config is fine on Pixel and Samsung; on the rest, we'd ship dropped audio mid-call. We added a foreground service with a persistent notification while a call is active, which keeps the OS from suspending the audio session.
- CallKit integration on iOS. iOS 17 tightened the rules around when you can use CallKit (the system's native call UI). If you don't show CallKit for VoIP calls, push delivery degrades. If you show it incorrectly, you get App Store review pushback. The integration took a week of getting it just right.
- Audio routing across Bluetooth/wired headsets. When a user plugs in headphones mid-call, the audio session has to renegotiate routes. CometChat handles most of this; the edge case of "user disconnects Bluetooth and we have to fall back to the speaker without dropping the call" needed glue.
The Three Failure Modes That Matter
There are a million ways a call can fail. Most don't matter. Three do, and we built specifically for them.
1. Network flakiness on the user side
Most of our users are on mobile data. A non-trivial fraction are on connections that drop briefly multiple times during a 15-minute call.
We trigger the "audio reconnecting..." UI when packets drop for more than 1.5 seconds, escalate to a full-screen "your astrologer is reconnecting" state at 5 seconds, and hard-end the session at 30 seconds. The metric we watch is RTC connection-state transitions per session — anything more than two reconnects in a 15-minute call gets logged for analysis. We don't yet have alerting on it; that's queued.
2. Astrologer dropping mid-session
When the astrologer's app crashes or loses connection, we don't want the user staring at a frozen screen.
Session state lives in two places: a transient state machine on the user app (current call ID, last known wallet meter, astrologer profile) and a durable record in the backend (call session ID, started timestamp, expected end). When the astrologer disconnects, the backend marks the session paused and starts a 30-second grace timer. The user app polls for state every 2 seconds during pause and shows the appropriate UI. If the astrologer reconnects, the session resumes with the same call ID and the wallet meter unfreezes. If they don't, the backend ends the session, finalizes the wallet, and the user app gets a "call ended unexpectedly" terminal state with auto-credit for the unused minutes.
3. AI fallback when a human isn't available
This is the most interesting one. If a user starts a call and the assigned astrologer doesn't pick up within a configurable window (currently 30 seconds), we want to gracefully offer the AI Astrologer as an alternative — not as a degraded substitute, but as a real choice.
The handoff is offered to the user, not to the astrologer. After 30 seconds of unanswered ringing, the user sees: "Your astrologer isn't picking up. Want to try our AI astrologer instead?" with options to wait, switch, or end. If they switch, the AI astrologer session inherits the user's question and any context already typed in chat. The astrologer's no-show counts toward their reliability score; repeat misses degrade their visibility in matching.
What We Got Wrong
The first version of our recording pipeline lost the last 3-5 seconds of every call.
The bug was that we finalized the recording stream on the sessionEnded CometChat event, which fires before the audio buffer fully drains. By the time the recording was uploaded, the tail of the conversation was gone. The fix was a two-step shutdown: on sessionEnded, we mark the recording for finalization but wait for an explicit audioStreamDrained event before closing the file.
The whole tail-loss thing was invisible to engineers — we never re-listened to test calls all the way through. It was caught by an astrologer reviewing one of her own recordings and noticing that the user's last sentence wasn't there. Good reminder that real users find the things synthetic tests don't.
What's Next
Three things on the roadmap:
- Video calls. Most of the asks come from astrologers who want to read body language during live readings.
- Regional media routing for South Asian users. Current routing is global. A Mumbai user talking to a Mumbai astrologer should not depend on a US relay.
- Lower-bandwidth audio codecs for users on patchy networks where the standard codec degrades badly under packet loss.