Swiss...
Self-Hosted Encrypted Messaging on Swiss VPS
Deploy a private Matrix homeserver on Swiss infrastructure — where end-to-end encryption meets data protection law. Full build from bare server to federated messaging, including the problems most guides skip.
October 26, 2026
by SwissLayer 25 min read
Self-Hosted Encrypted Messaging on Swiss VPS with Matrix and Element

The Problem with Every Messaging App You Use

Signal is good. WhatsApp claims end-to-end encryption. Telegram is… complicated. But all of them share one architectural fact: someone else runs the server. Your contact list, your message metadata, your group memberships, your online status — all of it lives on infrastructure you don’t control, in a jurisdiction you didn’t choose, operated by a company whose business model may not align with your privacy interests forever.

Matrix is an open protocol for decentralised, encrypted communication. Think of it as email for messaging — anyone can run their own server, and servers can talk to each other (federation), but you own your data and your encryption keys. The reference server implementation is called Synapse, and the most polished client is Element.

Run Synapse on a Swiss VPS and you get something no commercial messaging service offers: end-to-end encrypted messaging on infrastructure you own, in a country whose data protection laws — the FADP — are among the strictest on Earth, with no advertising company in the loop and no backdoor mandate in the legal framework.

This guide builds a production Matrix homeserver from scratch on Debian 12. It’s written for someone who can SSH into a server and edit config files. Where we hit problems during the real build, we left them in.

Why Switzerland for a Messaging Server

Before we touch the keyboard, the jurisdiction question matters more for messaging than almost any other service.

What Swiss law gives you:

• The Federal Act on Data Protection (FADP) treats personal data — including communication metadata — as fundamentally protected. There’s no mass-surveillance mandate equivalent to the US FISA Section 702 or the UK’s Investigatory Powers Act.
• Switzerland is not in the EU, not in the Five/Nine/Fourteen Eyes intelligence-sharing alliances, and not subject to EU data-retention directives.
• Law enforcement access requires a Swiss court order. Foreign government requests go through Mutual Legal Assistance Treaties (MLAT), which adds layers of judicial review and takes months, not minutes.
• There’s no law requiring hosting providers to build decryption backdoors or weaken encryption. If your server runs end-to-end encryption (and Matrix does), even a lawful seizure of the hardware yields ciphertext.

None of this makes your server “above the law.” It means the law is written by a country that considers privacy a constitutional right, not a feature toggle. That’s the foundation you’re building on.

What hosting in Switzerland gives you technically:

• Swiss data centres are connected to major European internet exchanges with sub-10ms latency to Frankfurt, Amsterdam, and Milan.
• The power grid runs largely on hydroelectric — cheap, green, reliable.
• Physical security standards in Swiss facilities are among the highest globally, often exceeding Tier III requirements.

What You’re Building

The finished system has four layers:

ComponentJob
Debian 12The operating system. Stable, minimal, predictable.
SynapseThe Matrix homeserver. Handles accounts, rooms, encryption key exchange, and federation.
PostgreSQLThe database. Synapse’s default SQLite works for testing but buckles under real use.
nginx + CertbotReverse proxy with HTTPS. Terminates TLS, handles .well-known delegation, proxies to Synapse.
Element WebThe browser-based client. Optional but useful — users can also use Element desktop/mobile apps.

Placeholders — replace with your own:

PlaceholderMeaning
matrix.example.comThe server hostname (where Synapse actually runs)
example.comYour Matrix domain (the @user:example.com part)
chat.example.comWhere Element Web is served (optional)
203.0.113.5Your server’s public IP

Step 1 — Server and Base System

Provision a Swiss VPS with at least:

• 2 vCPUs, 4 GB RAM (Synapse is memory-hungry during federation; 2 GB is the absolute floor for a personal server, 4 GB for a small team)
• 40 GB SSD minimum (media uploads and the database grow fast; plan 100 GB+ for anything beyond personal use)
• Debian 12 minimal install

apt update && apt full-upgrade -y
apt install -y curl wget gnupg lsb-release ca-certificates sudo ufw nano
timedatectl set-timezone Europe/Zurich

Step 2 — Firewall

Lock down everything except what Matrix needs:

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp    # SSH — restrict to your IPs in production
ufw allow 80/tcp    # HTTP (certificate validation + redirect)
ufw allow 443/tcp   # HTTPS (client connections)
ufw allow 8448/tcp  # Matrix federation (server-to-server)
ufw enable

Port 8448 is critical. That’s how other Matrix homeservers reach yours for federation — the ability to message users on other servers. Without it, you’re an island.

Production hardening: replace ufw allow 22/tcp with rules for your specific management IPs only. An open SSH port invites brute-force noise.

Step 3 — PostgreSQL

Synapse ships with SQLite support, but the official documentation warns against it for any real deployment. SQLite locks the entire database on writes — a single busy room can stall everything. PostgreSQL handles concurrent access properly.

apt install -y postgresql postgresql-contrib

sudo -u postgres psql <<'SQL'
CREATE USER synapse_user WITH PASSWORD 'STRONG_DB_PASSWORD';
CREATE DATABASE synapse
  ENCODING 'UTF8'
  LC_COLLATE='C'
  LC_CTYPE='C'
  template=template0
  OWNER synapse_user;
SQL

⚠️ Gotcha — the collation. The LC_COLLATE='C' and LC_CTYPE='C' are not optional. Synapse requires the C locale for its database. If you create the database with your system’s default locale (often en_US.UTF-8), Synapse will refuse to start with a cryptic locale mismatch error. Using template=template0 is what allows overriding the locale from your system default.

Generate your database password beforehand:

openssl rand -base64 24

Step 4 — Install Synapse

Use the official Matrix.org Debian repository. The version in Debian’s own repos lags significantly:

wget -qO /usr/share/keyrings/matrix-org-archive-keyring.gpg \
  https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] \
  https://packages.matrix.org/debian/ $(lsb_release -cs) main" \
  > /etc/apt/sources.list.d/matrix-org.list

apt update
apt install -y matrix-synapse-py3

During installation, the package asks for your server name. Enter example.com (your Matrix domain, not the machine hostname). This is permanent — changing it later means starting over with a fresh database.

⚠️ Gotcha — server_name is forever. The server_name you set during installation becomes part of every user ID (@user:example.com), every room ID, and every event your server signs. It’s cryptographically baked into the federation protocol. You cannot change it without losing all history and all user identities. Choose carefully. Use your main domain, not the machine hostname.

Step 5 — Configure Synapse

The main config file is /etc/matrix-synapse/homeserver.yaml. Open it and set the critical sections:

nano /etc/matrix-synapse/homeserver.yaml

Database — switch from SQLite to PostgreSQL:

database:
  name: psycopg2
  args:
    user: synapse_user
    password: "STRONG_DB_PASSWORD"
    database: synapse
    host: localhost
    cp_min: 5
    cp_max: 10

Listeners — set up the client and federation ports:

listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    bind_addresses: ['127.0.0.1']
    resources:
      - names: [client, federation]
        compress: false

Why bind to 127.0.0.1 only: Synapse listens on localhost, and nginx (the reverse proxy) handles all external connections with proper TLS. Synapse never touches the public network directly. The x_forwarded: true tells Synapse to trust the X-Forwarded-For header from nginx for rate limiting and logging.

Registration and security settings:

# Disable open registration — you control who gets accounts
enable_registration: false

# Allow registration via shared secret (for admin account creation)
registration_shared_secret: "GENERATE_A_LONG_RANDOM_STRING"

# Media storage
media_store_path: /var/lib/matrix-synapse/media
max_upload_size: 100M

# URL previews (optional but nice)
url_preview_enabled: true
url_preview_ip_range_blacklist:
  - '127.0.0.0/8'
  - '10.0.0.0/8'
  - '172.16.0.0/12'
  - '192.168.0.0/16'
  - '100.64.0.0/10'
  - '192.0.0.0/24'
  - '169.254.0.0/16'
  - '198.18.0.0/15'
  - '::1/128'
  - 'fe80::/10'
  - 'fc00::/7'

# Retention — auto-delete old messages (optional, privacy-first)
retention:
  enabled: true
  default_policy:
    min_lifetime: 1d
    max_lifetime: 365d
  allowed_lifetime_min: 1d
  allowed_lifetime_max: 365d

Generate the registration secret:

openssl rand -base64 32

⚠️ Gotcha — the URL preview blacklist. Without the IP blacklist above, the URL preview feature becomes a server-side request forgery (SSRF) vector. A malicious user could send a message containing http://169.254.169.254/latest/meta-data/ and your server would dutifully fetch it and preview the contents — potentially leaking cloud provider metadata. The blacklist blocks all private and link-local ranges. Don’t skip it.

Step 6 — nginx Reverse Proxy

nginx sits in front of Synapse, terminates TLS, and routes both client API traffic (port 443) and federation traffic (port 8448) to Synapse’s local listener.

apt install -y nginx certbot python3-certbot-nginx

Get the certificate first (nginx needs to serve port 80 for the challenge):

certbot certonly --nginx -d matrix.example.com \
  --email admin@example.com --agree-tos --no-eff-email

If your Matrix domain (example.com) is hosted on the same server, get that certificate too:

certbot certonly --nginx -d example.com \
  --email admin@example.com --agree-tos --no-eff-email

Now create the nginx configuration:

cat > /etc/nginx/sites-available/matrix.conf <<'NGINX'
# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name matrix.example.com;
    return 301 https://$host$request_uri;
}

# Client API (port 443)
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name matrix.example.com;

    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Content-Security-Policy "frame-ancestors 'none'" always;

    client_max_body_size 100M;

    location ~ ^(/_matrix|/_synapse/client) {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        proxy_read_timeout 600s;

        # Synapse responses can be large (media, sync)
        proxy_buffering off;
    }
}

# Federation (port 8448)
server {
    listen 8448 ssl;
    listen [::]:8448 ssl;
    http2 on;
    server_name matrix.example.com;

    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        proxy_read_timeout 600s;
        proxy_buffering off;
    }
}
NGINX

ln -sf /etc/nginx/sites-available/matrix.conf /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx

Step 7 — .well-known Delegation

Here’s the part that trips up almost everyone. Your Matrix user IDs are @user:example.com, but Synapse runs on matrix.example.com. Other servers need to know where to find yours. This is called delegation, and it uses .well-known files served from your main domain.

You need to serve two JSON files from https://example.com:

# On the server hosting example.com, create:

mkdir -p /var/www/html/.well-known/matrix

cat > /var/www/html/.well-known/matrix/server <<'EOF'
{
    "m.server": "matrix.example.com:443"
}
EOF

cat > /var/www/html/.well-known/matrix/client <<'EOF'
{
    "m.homeserver": {
        "base_url": "https://matrix.example.com"
    }
}
EOF

Make sure your web server (on example.com) serves these with the right CORS headers. In the nginx config for example.com:

location /.well-known/matrix/ {
    default_type application/json;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
    add_header Access-Control-Allow-Headers 'X-Requested-With, Content-Type, Authorization';
}

⚠️ Gotcha — the client file needs CORS. The /.well-known/matrix/client file is fetched by web browsers (Element Web runs in the browser), so it must include Access-Control-Allow-Origin: *. Without it, Element shows “Can’t find your homeserver” even though the server file (fetched server-to-server, no CORS needed) works fine. This mismatch is the #1 cause of “federation works but login doesn’t” reports.

Test both endpoints:

curl -s https://example.com/.well-known/matrix/server | python3 -m json.tool
curl -sI https://example.com/.well-known/matrix/client | grep -i access-control

Step 8 — Start Synapse and Create Your Account

systemctl enable --now matrix-synapse
systemctl status matrix-synapse --no-pager

Check the logs for errors:

journalctl -u matrix-synapse -n 50 --no-pager

If it’s running, create your admin account using the registration secret:

register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml \
  http://localhost:8008

It will ask for username, password, and whether to make the user an admin. Say yes to admin for your first account.

⚠️ Gotcha — registration_shared_secret must match. The register_new_matrix_user tool reads the shared secret from homeserver.yaml. If you edited the file but didn’t restart Synapse, the running process has the old secret and registration fails with a generic HMAC error. Always systemctl restart matrix-synapse after config changes.

Verify the API responds:

curl -s https://matrix.example.com/_matrix/client/versions | python3 -m json.tool

You should see a JSON object listing supported Matrix spec versions.

Step 9 — Element Web (Optional but Recommended)

Element is the most mature Matrix client. Installing the web version gives your users browser access without installing anything:

cd /tmp
ELEMENT_VERSION="1.11.85"  # check latest at https://github.com/element-hq/element-web/releases
wget "https://github.com/element-hq/element-web/releases/download/v${ELEMENT_VERSION}/element-v${ELEMENT_VERSION}.tar.gz"
tar xzf "element-v${ELEMENT_VERSION}.tar.gz"
mv "element-v${ELEMENT_VERSION}" /var/www/element

cp /var/www/element/config.sample.json /var/www/element/config.json

Edit /var/www/element/config.json:

{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.example.com",
            "server_name": "example.com"
        }
    },
    "disable_custom_urls": true,
    "disable_guests": true,
    "brand": "Element",
    "default_theme": "dark"
}

Add an nginx server block for Element (get its certificate first):

certbot certonly --nginx -d chat.example.com \
  --email admin@example.com --agree-tos --no-eff-email
cat > /etc/nginx/sites-available/element.conf <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name chat.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name chat.example.com;

    ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    root /var/www/element;
    index index.html;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Content-Security-Policy "frame-ancestors 'self'" always;
    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        try_files $uri $uri/ /index.html;
    }
}
NGINX

ln -sf /etc/nginx/sites-available/element.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Step 10 — Verify Federation

Federation is the whole point — the ability to message users on other Matrix servers. Test it:

# Check from the Matrix Federation Tester
curl -s "https://federationtester.matrix.org/api/report?server_name=example.com" \
  | python3 -c "import sys,json; r=json.load(sys.stdin); print('OK' if r.get('FederationOK') else 'FAIL')"

Or visit https://federationtester.matrix.org/#example.com in a browser.

⚠️ Gotcha — federation fails but client works fine. The most common cause: port 8448 is blocked by the firewall (check ufw status), or the .well-known/matrix/server file points to the wrong hostname or port. Federation uses a completely different discovery path than client login. Test them separately. Another common one: the SSL certificate on port 8448 doesn’t match the hostname in the server file. Use the same certificate.

Step 11 — Hardening for Production

A messaging server is a high-value target. Harden it beyond the defaults.

Rate limiting (already default in Synapse, but verify):

# In homeserver.yaml — these are the defaults, adjust if needed
rc_message:
  per_second: 0.2
  burst_count: 10
rc_login:
  address:
    per_second: 0.17
    burst_count: 3
  account:
    per_second: 0.17
    burst_count: 3

Fail2ban for Synapse:

apt install -y fail2ban

cat > /etc/fail2ban/filter.d/synapse.conf <<'EOF'
[Definition]
failregex = ^.* Received request: POST /_matrix/client/.*/login\b.*"request_ip": "".*$
            ^.* Failed password login.*remote_addr=.*$
ignoreregex =
EOF

cat > /etc/fail2ban/jail.d/synapse.conf <<'EOF'
[synapse]
enabled = true
port = 443,8448
filter = synapse
logpath = /var/log/matrix-synapse/homeserver.log
maxretry = 5
bantime = 3600
findtime = 600
EOF

systemctl restart fail2ban

Disable federation room directory (privacy measure):

By default, other servers can browse your public rooms. If you’re running a private server, disable this:

# In homeserver.yaml
allow_public_rooms_over_federation: false

Restrict room creation to admins (small team servers):

If you want tight control over what exists on your server:

# In homeserver.yaml — uncomment or add
# Only server admins can create rooms
# Regular users can still be invited to rooms
room_default_power_levels:
  events_default: 0
  state_default: 50

Media retention:

Left alone, Synapse stores every image, video, and file uploaded to any room you’re in — including federated media from other servers. This accumulates fast:

# In homeserver.yaml
media_retention:
  remote_media_lifetime: 90d
  local_media_lifetime: 365d

Step 12 — Backups

Your messaging server holds conversations, encryption keys, and uploaded media. Back it up properly.

What to back up:

• PostgreSQL database — the core. Contains all room state, user accounts, and message history.
• Synapse media store — /var/lib/matrix-synapse/media/ — uploaded files, images, avatars.
• Synapse config and signing key — /etc/matrix-synapse/ and especially homeserver.signing.key. Lose the signing key and your server’s federation identity is destroyed.
• Let’s Encrypt certificates — /etc/letsencrypt/ (these auto-renew, but backing them up avoids rate-limit issues during recovery).

#!/bin/bash
# /usr/local/bin/backup-matrix.sh
BACKUP_DIR="/var/backups/matrix"
DATE=$(date +%Y%m%d-%H%M)

mkdir -p "$BACKUP_DIR"

# Database
sudo -u postgres pg_dump synapse | gzip > "$BACKUP_DIR/synapse-db-${DATE}.sql.gz"

# Config and signing key
tar czf "$BACKUP_DIR/synapse-config-${DATE}.tar.gz" /etc/matrix-synapse/

# Media (incremental with rsync is better for large stores)
tar czf "$BACKUP_DIR/synapse-media-${DATE}.tar.gz" /var/lib/matrix-synapse/media/

# Retention — keep 14 days locally
find "$BACKUP_DIR" -name "*.gz" -mtime +14 -delete

Schedule it:

chmod +x /usr/local/bin/backup-matrix.sh
echo "0 3 * * * root /usr/local/bin/backup-matrix.sh" >> /etc/cron.d/matrix-backup

⚠️ Critical: local backups alone are not backups. If the server dies, the backups die with it. Copy them off-server — to a second Swiss dedicated server, to encrypted object storage, or to a BorgBackup repository. See our encrypted backup guide for a full off-server backup architecture.

Step 13 — Performance Tuning for Real Use

Synapse on default settings works for a few users. Under real load — especially with federated rooms — it needs tuning.

PostgreSQL tuning:

cat > /etc/postgresql/15/main/conf.d/synapse.conf <<'EOF'
shared_buffers = 1GB
effective_cache_size = 3GB
work_mem = 16MB
maintenance_work_mem = 256MB
max_connections = 200
random_page_cost = 1.1
effective_io_concurrency = 200
wal_buffers = 64MB
checkpoint_completion_target = 0.9
EOF

systemctl restart postgresql

Synapse caching:

# In homeserver.yaml
caches:
  global_factor: 1.0
  per_cache_factors:
    get_users_in_room: 2.0
    get_current_state_event: 2.0

Synapse worker architecture (advanced):

For servers handling more than ~50 concurrent users or participating in large federated rooms, Synapse supports splitting work across multiple worker processes. The main bottleneck is the sync endpoint (what clients poll constantly). Moving it to a dedicated worker dramatically reduces load on the main process. This is documented in the Synapse workers documentation and is worth implementing once you hit performance limits on a single process.

Step 14 — Monitoring

A messaging server that goes down silently is worse than no server. Set up basic monitoring:

# Quick health check script
cat > /usr/local/bin/check-matrix.sh <<'BASH'
#!/bin/bash
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
  https://matrix.example.com/_matrix/client/versions)

if [ "$HTTP_CODE" != "200" ]; then
    echo "ALERT: Matrix homeserver returned HTTP $HTTP_CODE" | \
      mail -s "Matrix Server Alert" admin@example.com
fi
BASH

chmod +x /usr/local/bin/check-matrix.sh
echo "*/5 * * * * root /usr/local/bin/check-matrix.sh" >> /etc/cron.d/matrix-monitor

For proper monitoring, expose Synapse’s Prometheus metrics (add to homeserver.yaml):

enable_metrics: true
metrics_port: 9100
# Bind metrics to localhost only — never expose externally
metrics_bind_host: 127.0.0.1

Then scrape http://127.0.0.1:9100/_synapse/metrics with Prometheus and build Grafana dashboards. The Synapse project provides a reference Grafana dashboard.

Understanding End-to-End Encryption in Matrix

Matrix’s encryption deserves explanation because it’s fundamentally different from TLS (the padlock in your browser).

TLS encrypts data between your device and the server. The server sees everything in plaintext — it has to, to route and store messages.

Matrix E2EE (end-to-end encryption, based on the Olm/Megolm protocol — the same cryptographic ratchet that underpins Signal’s encryption) encrypts messages on the sender’s device and decrypts them only on the recipient’s device. The server handles routing, stores the ciphertext, but cannot read the content. Not the server admin, not a subpoena, not a hardware seizure.

In Matrix, E2EE is enabled per-room and is the default for direct messages. Here’s what that means practically:

• Messages are encrypted: content, attachments, images, reactions.
• Metadata is not: who is in the room, when messages are sent, room names (unless encrypted). The server needs this to function.
• Key verification matters: Element lets users verify each other’s devices using emoji comparison or QR codes. Without verification, you’re trusting that the server correctly distributes keys — which is where having your own server becomes powerful. On someone else’s server, key distribution is a trust point. On your server, you control it.
• Key backup: if a user loses all their devices without setting up key backup (Secure Key Backup in Element), encrypted message history is permanently unreadable. Configure key backup during onboarding.

Bridges — Connect to Other Networks

One of Matrix’s most practical features: bridges let you use your Matrix client to talk to people on Signal, WhatsApp, Telegram, Discord, Slack, and IRC without installing those apps or giving them your phone number from your main device.

The most reliable bridges:

BridgeNetworkNotes
mautrix-signalSignalFull double-puppeting, rich media, reactions. Requires a linked Signal device.
mautrix-whatsappWhatsAppWorks via WhatsApp Web protocol. Link via QR code.
mautrix-telegramTelegramFull-featured, including Telegram groups and channels.
mautrix-discordDiscordDMs and server channels.

Bridge installation is its own article — each has specific requirements — but the architecture is straightforward: the bridge runs as a separate service, registers as an “application service” with Synapse, and creates Matrix rooms that mirror conversations on the other network. Your messages go through your Matrix server (encrypted), the bridge decrypts only what it needs to forward, and delivers to the other network.

Privacy note on bridges: the bridge itself must decrypt messages to forward them to the other network. This is unavoidable — WhatsApp can’t read Megolm ciphertext. Run bridges only on your own server, and understand that the bridge process has access to the plaintext of bridged messages. Conversations that stay within Matrix remain fully E2EE.

Operational Notes

Updating Synapse:

apt update && apt upgrade -y matrix-synapse-py3
systemctl restart matrix-synapse

Read the changelog before upgrading. Synapse occasionally introduces breaking config changes or database migrations that increase restart time.

Updating Element Web:

cd /tmp
ELEMENT_VERSION="x.x.x"  # check latest release
wget "https://github.com/element-hq/element-web/releases/download/v${ELEMENT_VERSION}/element-v${ELEMENT_VERSION}.tar.gz"
tar xzf "element-v${ELEMENT_VERSION}.tar.gz"
cp /var/www/element/config.json "/tmp/element-v${ELEMENT_VERSION}/config.json"
rm -rf /var/www/element
mv "/tmp/element-v${ELEMENT_VERSION}" /var/www/element

Database maintenance:

# Synapse accumulates state resolution data. Periodic VACUUM keeps PostgreSQL healthy:
sudo -u postgres vacuumdb --analyze synapse

# Check database size:
sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size('synapse'));"

Log rotation:

Synapse logs can grow large in federated environments. The default logrotate config should handle it, but verify:

ls -lh /var/log/matrix-synapse/
cat /etc/logrotate.d/matrix-synapse

What You’ve Built

You now run a private, encrypted messaging server on Swiss infrastructure:

• ✅ End-to-end encrypted messaging — messages are plaintext only on sender and recipient devices, never on the server
• ✅ Swiss jurisdiction — FADP protection, no mass-surveillance mandates, court-order-only access
• ✅ Federation — you can message anyone on any Matrix server worldwide, or keep it private
• ✅ No third-party dependencies — no app store account, no phone number requirement, no corporate intermediary
• ✅ Hardened — rate-limited, fail2ban-protected, HSTS-enforced, SSRF-mitigated
• ✅ Bridge-ready — can connect to Signal, WhatsApp, Telegram, Discord through your server

The honest trade-off: you’re now responsible for uptime, updates, and backups. A messaging server that goes down means people can’t communicate — that’s a higher-stakes service than a file sync. Plan for it.

When Self-Hosting Isn’t the Right Call

Running a Matrix homeserver is satisfying and genuinely private. But it’s also ongoing work: Synapse updates, database maintenance, monitoring, backup verification, certificate renewals, security patches, and being on call when federation breaks at 2 AM.

For a personal server or a small team, it’s manageable. For an organisation that needs messaging uptime guarantees but doesn’t have a dedicated sysadmin, it’s a commitment that shouldn’t be underestimated.

If you want the privacy of Swiss-hosted messaging infrastructure without managing it yourself, SwissLayer runs dedicated servers in Switzerland with full management — we handle the uptime, the patches, and the 2 AM pages so you don’t have to.

Questions about deploying Matrix on Swiss infrastructure, or about managed hosting for messaging workloads? Get in touch.