How we built DeskTrust Connect: mediasoup, coturn, and a single VPS
By DeskTrust Engineering
We built DeskTrust Connect end-to-end on a compressed timeline — 10 milestones (M0 through M9), all code-complete. Here\'s what we chose, what we punted on, and what surprised us. Written for engineering readers considering similar work.
The stack
- WebRTC + mediasoup for real-time media. SFU, not mesh — critical past 2 participants.
- coturn for STUN + TURN. Time-limited HMAC credentials issued per session.
- Socket.io for signaling and cross-tab presence. Redis pub/sub for cross-process fanout.
- ffmpeg for recording. Pass-through WebM (VP8 + Opus, no transcode) writes straight from mediasoup\'s PlainRtpTransport.
- Standalone Node.js ESM process for the connect-server (Socket.io + mediasoup + recording). Deliberately outside the Next.js app so a WebRTC storm can\'t take down /dashboard.
Choices we made
Self-hosted instead of managed
We considered LiveKit Cloud, Daily.co, and Twilio Video for the media stack. All would have been faster to ship. We went self-hosted because (a) enterprise customers (Kuwait Oil pilot) have data-residency requirements that most managed services complicate, and (b) our margins are already good enough at $12/user/month that per-minute media pricing would have compressed them.
The trade: 3-4 weeks more of build. And now we own an SFU. We knew what we were signing up for.
Consent-first screen viewing
The plan flagged this as the highest legal-complexity milestone. We invested more design time here than the milestone budget suggested and shipped a working implementation with jurisdiction- aware disclosures, immutable audit log, and no way to silently view. See the deep-dive.
WebM over MP4 for recording
WebM (VP8+Opus) plays natively in modern browsers and matches mediasoup\'s codec defaults exactly. Pass-through recording means ffmpeg is a router, not an encoder — critical for keeping the 2-core VPS alive during real recording load.
MP4 (H.264+AAC) requires transcoding and is v1.1 for customers whose e-discovery workflows demand it.
Per-org gating, not plan-level
Connect is an add-on gated by Organization.connectAddOnActive, not by base plan. Any plan can enable it. This kept the pricing conversation simple ("$12 on top of what you pay") and avoided baking Connect into plan-tier feature lists that customers memorize.
Things that surprised us
The 2-core VPS is genuinely a bottleneck
We knew this going in — the plan warned about it starting at M5. But it\'s more real than we expected: any group call > 4 participants on the shared box will affect screenshot upload latency in the base DeskTrust product. Recording piles on more CPU.
Solution: customers running Connect at scale should either upgrade the VPS or split media services onto a companion box. Our default pilot deployment guidance is now "add a $40/mo Hetzner CCX23 companion before enabling Connect at more than 5 concurrent users."
getDisplayMedia + user gestures + async server calls
Browsers require navigator.mediaDevices.getDisplayMedia() to be invoked from a user gesture — a click. That\'s straightforward for a "Share screen" button. It\'s not straightforward for the manager-view approve flow, which conceptually goes:
- Employee clicks Approve
- Server creates a session
- Employee\'s client picks a screen
- Media starts flowing
Step 3 must happen inside the same task as step 1 for the browser to honor it. Doing the server-side call between them breaks the gesture chain in Chrome.
Fix: pre-obtain the media stream inside the click handler (step 3 happens synchronously with step 1), then submit the approval + stream reference to the server together. Threaded that through the useConnectCall hook with a providedScreenStream option on the bootstrap function.
mediasoup\'s active-speaker observer just works
Router.createAudioLevelObserver was the single simplest piece of code that produced the largest UI improvement. Throttled at 500ms, broadcast via Socket.io, rendered as a colored ring on the tile — a 6-person group call becomes readable that wouldn\'t be otherwise.
Consent-first isn\'t just legal, it\'s design
Once we committed to consent-first screen viewing, other things fell out cleanly:
- Employee-side gets a small floating indicator, not a fullscreen call view — because the whole point is they should keep working
- The manager\'s reason shows in the approval modal — because context-aware consent is actually consent
- Every event writes an audit row — because a consent-first product needs consent evidence
- The manager\'s "waiting for approval" screen has a Cancel button — because withdrawing a request is a first-class action
Whole surface becomes coherent. We didn\'t have to force each of these individually; they followed from the principle.
What we punted on for v1
- S3 offload for recordings past 7 days (local disk works for pilot, will bite at scale)
- Multi-participant tiled recording composition (v1 records one audio + one video track)
- Slack-style channels beyond org (external participants)
- Real ringtone audio asset (tab-title flash only)
- Native mobile apps (browser-only, iOS Safari has some quirks)
- Screen-view remote control (view-only in v1)
- Full-text search (LIKE-based; upgrade to MySQL FULLTEXT when a room exceeds 10k messages)
None of these are v2 material. All are v1.1 tickets waiting for customer signal.
What we\'d change
If we did it again on a 6-month timeline instead of a compressed one, we\'d put more time into mobile PWA polish and skip the initial VPS-integrated deployment in favor of separate media infrastructure from day one. The rest — self-hosted vs. managed, consent-first, WebM recording — we\'d make the same calls.
Try it. Break it. Tell us what surprised you.