← All Connect posts
Engineering·· 9 min read

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:

  1. Employee clicks Approve
  2. Server creates a session
  3. Employee\'s client picks a screen
  4. 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.

Included free · paid DeskTrust plans

Turn on chat, calls, and screen sharing — no new bill, no new login.

Cancel your Slack + Zoom + TeamViewer trio. Consolidate on the tool your team already uses.