One of the clearer lessons from this phase was that working chat does not automatically mean working calls.

For newer Matrix client flows, Synapse plus TURN was not enough.

To get closer to a usable calling setup, I needed a separate RTC-focused stack.

Why I Split It Out

I did not want to bolt real-time media services into the main Matrix stack without clear boundaries.

So I treated MatrixRTC as its own component group:

  • a media transport service,
  • a small auth service for issuing RTC-related tokens,
  • its own container set,
  • its own runtime config,
  • and its own Unix-user boundary.

That matched the way I was already operating the rest of the infrastructure.

The Main Design Constraint

Calls are more demanding than normal API traffic.

I had to think about:

  • reverse-proxied control traffic,
  • media ports,
  • possible TCP fallback,
  • and how the client learns where the RTC service lives.

That meant the work was not just "start another container." It was really a combination of:

  • app discovery,
  • transport design,
  • auth integration,
  • and firewall discipline.

Why I Kept It Separate From The Main Matrix User

Using a separate stack identity for the RTC side made the whole system easier to reason about.

It gave me:

  • separate logs,
  • separate Vault access,
  • separate secrets,
  • and less chance that a call-specific problem would spill into the main homeserver runtime.

That separation also made it easier to decide what the RTC stack was allowed to know and what it was not.

The Real Takeaway

The call stack taught me that "feature complete" infrastructure is often just layered infrastructure.

The moment I stopped treating calls as a little Synapse add-on and started treating them as a proper service boundary, the architecture became much clearer.

That pattern repeated across the whole Matrix project:

  • split responsibilities,
  • keep identities narrow,
  • and accept that the clean design usually needs one more component than the first quick draft.