Soul33 needed a real-time group chat and voicemail system to support wellness communities of 200+ concurrent users. After evaluating Firebase Realtime Database, Supabase Realtime, and raw WebSocket, we chose a custom WebSocket server — lowest latency, full control over message schemas, and no vendor lock-in on message pricing at scale.
Connection lifecycle — the hard part
The trickiest part is managing the connection across app state changes. On iOS, the OS kills the socket after ~30 seconds in the background. On Android, aggressive battery optimisation on OEM ROMs (Xiaomi, Samsung) terminates connections even sooner. Our WebSocket context provider listens to AppState changes and implements exponential backoff reconnection with a maximum 30-second ceiling.
let backoff = 1000;
const MAX_BACKOFF = 30000;
const reconnect = async () => {
await delay(backoff);
socket = new WebSocket(WS_URL);
socket.onopen = () => {
backoff = 1000; // reset on success
syncFromSeq(lastSeq); // replay missed messages
};
socket.onerror = () => {
backoff = Math.min(backoff * 2, MAX_BACKOFF);
reconnect();
};
};Message deduplication
Because we replay messages from the last known sequence number on reconnect, the client may receive duplicates. We maintain a Set of processed message IDs in Redux state, checking each incoming message before dispatching to the store. IDs are stored in a sliding window of the last 500 messages to avoid unbounded memory growth.
// In chatSlice reducer
const processedIds = new Set(state.processedIds);
if (processedIds.has(message.id)) return; // duplicate
processedIds.add(message.id);
// Sliding window — keep last 500 only
if (processedIds.size > 500) {
const oldest = [...processedIds][0];
processedIds.delete(oldest);
}
state.messages.push(message);
state.processedIds = [...processedIds];Swipe-to-reply with Reanimated
Each message row is wrapped in a PanGestureHandler. A horizontal swipe beyond a 60dp threshold reveals a reply icon and stores the target message in local state as replyTarget. The reply preview bar appears above the text input. On send, the message payload includes a replyTo field with the original message ID and a 60-character preview snippet. Tapping a quoted reply scrolls the FlatList to that message using scrollToIndex and flashes a highlight animation.
Audio messages via S3
Voice messages record via react-native-audio-recorder-player, show a real-time waveform via react-native-audio-waveform, and upload to S3 with progress tracking. The S3 pre-signed URL is fetched from our backend first, then the file is PUT directly to S3 from the device — keeping our server out of the binary transfer path. The message is sent to the chat only after the S3 upload completes, including the audio duration for the playback UI.
At peak load, the system handled 200+ concurrent sessions with under 50ms average message latency measured from send to receipt. The key was optimistic UI updates — messages appear immediately client-side and are confirmed or rolled back based on the server ACK.