Self-Hosting Matrix: Federation, Calls, and What Nobody Tells You
I wanted a self-hosted chat platform that my friends could use without installing anything weird. Matrix with Element Web checked every box: it works in the browser, supports voice and video calls, has end-to-end encryption, and federates with other Matrix servers. The setup was mostly straightforward, but there were a few things that cost me hours because the documentation glosses over them.
Server Name Is Permanent
The most important decision you make is server_name in the Synapse config. This is the domain that appears in every user ID. If you set it to matrix.example.com, your users will be @name:matrix.example.com. If you set it to example.com, they will be @name:example.com.
You want the bare domain. Shorter IDs look better and are easier to share. But here is the catch: once you set the server name and create the first user, you cannot change it. Ever. The signing keys, room IDs, and event hashes are all tied to that name. Changing it means starting over.
I went with the bare domain and use well-known delegation to tell other servers that the actual homeserver lives at a subdomain.
Well-Known Delegation
When another Matrix server wants to talk to yours, it looks up your server name. If your server name is example.com but Synapse runs on matrix.example.com, the remote server needs a way to discover that.
You serve two JSON endpoints on the bare domain:
The server endpoint returns which host and port handles federation. The client endpoint returns where the client API lives. Both are tiny JSON responses that Caddy can serve inline with the respond directive. No files, no separate service.
The important detail that the docs barely mention: the client well-known response needs a CORS header. Specifically Access-Control-Allow-Origin: *. Without it, Element Web running in a browser cannot discover the homeserver. You will stare at a login screen that says it cannot reach the server and wonder what you did wrong.
Cloudflare and Federation
If your domain is behind Cloudflare, federation works fine as long as you set up the well-known endpoints correctly. Federation traffic goes through port 443, which Cloudflare proxies without issues.
The one thing to watch: Cloudflare has a 100-second timeout on the free plan. For a small personal server this is irrelevant, but if you ever end up in a large federated room, the initial sync could theoretically hit that limit.
I have been in rooms with several hundred members and never hit the timeout. But it is worth knowing about.
Voice and Video Calls
Element supports one-to-one and group calls, but they need a TURN server to relay media when direct peer-to-peer connections fail. This happens more often than you would think. Corporate firewalls, symmetric NAT, and mobile carriers all block direct WebRTC connections.
I already had a coturn server running for another project. Adding Matrix support was just a matter of generating a shared secret and putting it in the Synapse config. Synapse generates time-limited TURN credentials for each call using HMAC, which is more secure than static credentials.
The config in Synapse points to the TURN server by IP address, not hostname. This avoids DNS resolution issues. You list both UDP and TCP transport options so clients behind restrictive firewalls can fall back to TCP.
Testing calls is annoying because you need two separate accounts on two separate devices. You cannot call yourself. I created a test account, logged in on my phone, and called from my laptop. The first call had one-way audio because the TURN server was binding relay ports on Docker internal IPs instead of the public IP. Adding --relay-ip and --listening-ip flags to coturn fixed it immediately.
Registration and Invites
I started with invite-only registration using tokens. Synapse has a built-in token system where you generate a token, give it to someone, and they use it during registration. Each token can be limited to a single use.
Then I realized I was the only one creating accounts, so I just opened registration entirely. Synapse will complain about open registration without verification, but you can silence that with a config flag. For a small server with friends, the spam risk is minimal.
If spam does become a problem, switching back to token-based registration is a one-line config change. No need to over-engineer it upfront.
Password Policy
Synapse has a built-in password strength checker that rejects most passwords. It is aggressive. Passwords that I considered perfectly fine were being rejected. The policy can be disabled in the config, which I did immediately.
Element Web also has its own client-side password strength check that is separate from the server policy. Even after disabling the server policy, Element still complained. That one I could not disable through config. It is more of an annoyance than a blocker since it warns but does not prevent you from proceeding.
Resource Usage
Synapse is the heaviest thing on my server. At idle with a handful of users, it uses about 300 megabytes. During federation sync or when someone joins a large room, it can spike to a gigabyte.
The PostgreSQL database backing it uses another 70 megabytes. Element Web is negligible since it is just static files served by nginx in a container.
The config has a caches.global_factor setting that controls memory usage. The default is 0.5. Turning it down to 0.3 reduces memory at the cost of more database queries. For a small server the difference is not noticeable.
Was It Worth the Effort
Absolutely. My friends and I have a chat platform that we fully control. Voice calls work, screen sharing works, file sharing works. The web client means nobody has to install anything. And because it federates, anyone on any Matrix server can reach us if they know our addresses.
The setup took about two hours. Most of that was the TURN server configuration and testing calls. If you skip voice and video, you could have it running in thirty minutes.