All posts

Building Real-Time Audio Collaboration in React Native: Lessons from Boombox

Three years building a musician collaboration app taught me where real-time audio in React Native gets hard: signaling, iOS audio sessions, sync, and uploads that survive bad networks.

4 min read
React NativeWebRTCAudioSocket.ioAWS S3

From 2020 to 2023 I worked on Boombox, a collaboration app where musicians record audio and video together, chat in real time, and layer takes on top of each other's tracks. The prototype I built carried the company through its seed round, and the product grew to over 10,000 users. Music apps are an unforgiving domain: a chat message arriving 300 ms late is fine; a guitar track drifting 300 ms against the drums is garbage.

Here's where the real difficulty lived — mostly not where I expected.

WebRTC is the easy part; signaling is yours

react-native-webrtc gives you the media stack: capture, encode, peer connections. What it deliberately doesn't give you is signaling — the exchange of session descriptions and ICE candidates that lets two peers find each other. We ran that over Socket.io, which we already used for chat, and that turned out to be the right call: one stateful connection, one reconnection strategy, one place to reason about presence.

The design that survived contact with reality:

  • Socket.io rooms per collaboration session; joining a room triggers the offer/answer dance.
  • Every signaling message is idempotent and carries a session epoch, because mobile clients reconnect constantly — app backgrounded, network switched from Wi-Fi to LTE, phone locked. A stale ICE candidate applied after reconnect corrupts the session; the epoch check discards it.
  • TURN servers are not optional. A meaningful share of sessions — carrier-grade NAT, hotel Wi-Fi — never establish a direct peer connection. Without TURN relay as fallback, those users just think your app is broken.

The iOS audio session will humble you

The hardest bugs in three years were not JavaScript. They were AVAudioSession configuration on iOS. The audio session is global, stateful, and shared with the OS and every other app — and a music app needs modes that fight each other:

  • Recording wants the measurement-style input path, no system processing.
  • Monitoring (hearing yourself while recording) needs low-latency playback simultaneously — playAndRecord, with the right options or the output snaps to the earpiece instead of headphones.
  • Playback of the backing track while recording a new layer is both at once, and Bluetooth headphones add codec latency that wired ones don't have.

Two rules I paid for: first, own the session in one native-side manager with explicit states (idle, playback, record, layered) — never scatter category changes across features, because the last writer wins and debugging "why is audio quiet" across five call sites is misery. Second, handle interruptions (phone call, Siri, another app claiming audio) as first-class events that pause recording cleanly and tell the user, rather than pretending they don't happen and producing a broken take.

Sync: timestamps at the source, alignment at playback

Layered recording — drummer records, guitarist plays on top — is a synchronization problem wearing a music costume. What worked:

  1. Stamp every recording at capture time against a shared clock (server time negotiated at session start, with round-trip compensation), not "upload time" or client wall-clock, which drifts.
  2. Store the backing track's playback offset at the moment recording started with the take's metadata.
  3. Do the actual alignment at playback/mixdown from that metadata, and give users a manual nudge control (±10 ms steps) as the escape hatch, because Bluetooth output latency is a per-device unknown you cannot fully model.

The nudge control felt like an admission of defeat. Users loved it. Musicians already understand latency; give them the knob.

Uploads that survive real networks

Takes are large, and the people recording them are on mobile networks. Media handling on AWS S3 settled into this shape:

  • Multipart uploads with resume. A dropped connection at 90% must not restart the upload. Chunks retry independently with backoff.
  • Upload straight from the file path. Never read a recording into JS memory to send it — stream from disk, or a long take on a low-end Android device kills the app before the network gets a chance.
  • Local-first, then queue. The take is safe on disk the moment recording stops; sync to S3 is a background queue with visible per-item status. Musicians will tolerate "syncing…" for a long time. They will not tolerate losing a take they nailed.

What I'd tell someone starting this today

The stack has moved since 2020 — but the shape of the problem hasn't. The media layer is a solved dependency; your engineering actually lives in four places: signaling state under constant reconnection, native audio session management, a clock the whole session agrees on, and uploads designed for hostile networks. Budget the project that way — roughly in that order — and the demo-to-production gap stops being a canyon.