← Back to Blog

Matrix and Element: A Self-Hosted Slack or Teams Alternative for Your Company

Published · by RS Computers

Matrix Self-hosting Team chat

One of the most complete self-hosted Slack alternatives in 2026 is Matrix with Element: you run Synapse, the Matrix server, on a VPS with PostgreSQL, and your team uses Element in the browser, on the desktop or on their phones. Messages, files and accounts live on a server you control, there is no per-user fee, and private rooms can be end-to-end encrypted. In our lab the whole stack (Synapse, PostgreSQL, nginx and Element Web) used between 200 and 245 MB of RAM for a small team, and the install took one afternoon, mistakes included. Below is that afternoon, with every command we ran and what it printed.

Key facts, checked on 9 October 2026:

The afternoon at a glance

Our fictional company, example.com, has an admin and three colleagues: Anna, Besnik and Marco. The lab was a Debian 13 container with 2 CPU cores and 3 GB of RAM on one of our Amsterdam servers. Here is the plan; the real timings came close:

TimeStepResult
13:00Choose names and DNSUser IDs like @anna:example.com fixed for good
13:20Install Synapse and PostgreSQL29 seconds of apt in our lab
13:40Database, closed sign-up, federation offStrangers get "Registration has been disabled"
14:00Create everyone's account from a listFour accounts, random passwords
14:20Element Web and nginxBranded client on chat.example.com
14:50.well-known files on the main domainApps find the server from the email-like ID
15:10HTTPS, rooms, encryption, history importFirst encrypted room, 1,000 test messages

Matrix, Synapse and Element in plain words

Matrix is an open standard for chat, as email is for mail. A homeserver stores your users, rooms and messages; a client is the app people type into. Element is the best-known client, and Synapse is the reference homeserver, written in Python. A room is a Slack channel, and a space is a group of rooms, close to a Slack workspace. Federation lets your server talk to other Matrix servers, the way your mail server delivers to Gmail.

Synapse is not the only homeserver. This is how the matrix.org server list labels the main options today:

HomeserverLanguageStatus on matrix.orgNotes
SynapsePythonStableReference server, Debian packages, full admin API. What we used.
ContinuwuityRustStableCommunity fork in the Conduit family, light on RAM
TuwunelRustStableSuccessor to conduwuit, which is now listed as obsolete
ConduitRustBetaThe original small Rust server
DendriteGoBetaIn maintenance mode, "only security fixes are being applied" (README)

For a company we picked Synapse because it has signed packages for Debian 13, it supports PostgreSQL, and its admin API covers the boring jobs: creating accounts, resetting passwords and switching off a leaver's account.

What you stop paying Slack or Teams

Per-seat pricing grows with every hire. For a team of 25, using the list prices above:

OptionPrice per user25 users for one year
Slack Pro, monthly billing$8.75 a month$2,625
Slack Pro, yearly billing$7.25 a month$2,175
Slack Business+, yearly billing$15 a month$4,500
Teams Essentials, yearly$4.00 a month$1,200
Matrix on your own VPS$0One server, the same price for 5 or 50 people

These are US list prices from the vendors' pages on 9 October 2026, before tax. The real cost of Matrix is your time: one afternoon to set up and a few minutes a month for updates.

The data protection angle for EU companies

Under the GDPR, sending personal data out of the EU needs a legal basis (Regulation (EU) 2016/679, Chapter V). For US cloud tools that basis today is usually the EU-US Data Privacy Framework, which the European Commission adopted on 10 July 2023 and which covers US companies that have signed up to it (European Commission). Its predecessor, the Privacy Shield, was annulled by the EU Court of Justice in 2020, which is why many legal teams would rather not rely on a transfer framework at all.

Slack's answer is data residency, but only on Business+ and Enterprise, and even there member profiles and channel membership may sit in another region. With your own homeserver the messages, files and user list sit on one machine in the city you chose, and no chat vendor processes them. A server in Amsterdam or Dublin keeps the data inside the EU, and teams in Kosovo and the region can keep it in Prishtina. Regulated sectors use the same model: Germany's health care messenger standard, TI-Messenger, "builds on matrix" (gematik).

13:00 Names first: server name, subdomains and delegation

Decide this before you install anything. The server name becomes the last part of every user ID, so with the server name example.com your colleagues are @anna:example.com and @besnik:example.com. It cannot be changed later without starting over, so use your company domain, not a subdomain.

The server itself does not have to live on example.com. Your website probably already does. Matrix solves this with delegation: two tiny JSON files on https://example.com/.well-known/matrix/ tell other servers and apps "the Matrix server for this domain is matrix.example.com". So you need:

13:20 Install Synapse and PostgreSQL

Start from a fresh Debian 13 server and log in as root. The Synapse install guide says Debian 13 should use the packages from packages.matrix.org, because Debian's own package only exists for newer releases. The two debconf-set-selections lines answer the installer's questions in advance: your server name, and "no" to sending usage statistics.

# as root
apt update
apt install -y lsb-release wget apt-transport-https curl sudo jq openssl
wget -O /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" | tee /etc/apt/sources.list.d/matrix-org.list
apt update
echo "matrix-synapse-py3 matrix-synapse/server-name string example.com" | debconf-set-selections
echo "matrix-synapse-py3 matrix-synapse/report-stats boolean false" | debconf-set-selections
apt install -y matrix-synapse-py3 postgresql
systemctl is-active matrix-synapse postgresql

The last command should print active twice. In our lab the install took 29 seconds and gave us Synapse 1.162.0 (package 1.162.0+trixie1, with its own Python 3.13 in /opt/venvs/matrix-synapse, 278 MB on disk) and PostgreSQL 17.11.

13:40 Give Synapse a real database and close the doors

Out of the box Synapse uses SQLite, a database in a single file. The docs are blunt: "SQLite should not be used in a production server". Create a PostgreSQL user and database with the encoding and locale the Synapse Postgres guide requires, then tell Synapse to use them. Put your own settings in /etc/matrix-synapse/conf.d/, not in homeserver.yaml, so package upgrades never ask you to merge files.

# as root
DBPASS=$(openssl rand -hex 16)
sudo -u postgres psql -c "CREATE ROLE synapse_user LOGIN PASSWORD '$DBPASS';"
sudo -u postgres createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse_user synapse

cat > /etc/matrix-synapse/conf.d/database.yaml <<EOF
database:
  name: psycopg2
  args:
    user: synapse_user
    password: $DBPASS
    dbname: synapse
    host: localhost
    cp_min: 5
    cp_max: 10
EOF

cat > /etc/matrix-synapse/conf.d/company.yaml <<EOF
public_baseurl: https://matrix.example.com/
enable_registration: false
registration_shared_secret: $(openssl rand -hex 32)
federation_domain_whitelist: []
max_upload_size: 100M
EOF

chown root:matrix-synapse /etc/matrix-synapse/conf.d/*.yaml
chmod 640 /etc/matrix-synapse/conf.d/database.yaml /etc/matrix-synapse/conf.d/company.yaml
systemctl restart matrix-synapse
curl -s http://localhost:8008/_matrix/client/versions | head -c 80; echo

The psql line prints CREATE ROLE, and the last line prints the start of a list like {"versions":["r0.0.1","r0.1.0",.... Synapse then built 178 tables in PostgreSQL on its first start. What the second file does, line by line:

Check with ss -tlnp: Synapse (port 8008) and PostgreSQL (port 5432) listen only on 127.0.0.1 and ::1. Nothing but your web server will face the internet.

14:00 Create everyone's account from a list

With sign-up closed, the admin creates accounts. Put the usernames in a text file, one per line, and let a loop create each account with a random password:

# as root
cd /root
openssl rand -base64 18 > admin.pw
register_new_matrix_user -c /etc/matrix-synapse/conf.d/company.yaml -u admin --password-file admin.pw --admin http://localhost:8008

printf "anna\nbesnik\nmarco\n" > team.txt
mkdir -p passwords && chmod 700 passwords
while read -r name; do
  openssl rand -base64 18 > passwords/$name.pw
  register_new_matrix_user -c /etc/matrix-synapse/conf.d/company.yaml -u $name --password-file passwords/$name.pw --no-admin http://localhost:8008
done < team.txt

Each account prints Sending registration request... and Success!. Hand each person their password privately and ask them to change it in Element under Settings. Then prove the door is shut. A sign-up attempt through the API got this answer in our lab:

# as root, on the server
curl -s -X POST http://localhost:8008/_matrix/client/v3/register -H 'Content-Type: application/json' -d '{"username":"stranger","password":"Xx123456789xx"}'

Output: {"errcode":"M_FORBIDDEN","error":"Registration has been disabled"}.

14:20 Element Web on its own hostname

We first tried the release tarball from GitHub, then switched to Element's Debian repository, which Element documents and which updates with the rest of the system:

# as root
wget -O /usr/share/keyrings/element-io-archive-keyring.gpg https://packages.element.io/debian/element-io-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/element-io-archive-keyring.gpg] https://packages.element.io/debian/ default main" | tee /etc/apt/sources.list.d/element-io.list
apt update
apt install -y element-web nginx

That installed Element Web 1.12.30 into /usr/share/element-web (138 MB). Its settings file is /etc/element-web/config.json. The default points at matrix.org, so replace it with your own. This version locks the app to your server, hides the sign-up button, turns off guest access and removes the third-party "integration manager":

# as root
cat > /etc/element-web/config.json <<'EOF'
{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.example.com",
      "server_name": "example.com"
    }
  },
  "disable_custom_urls": true,
  "disable_guests": true,
  "brand": "Company Chat",
  "integrations_ui_url": null,
  "integrations_rest_url": null,
  "integrations_widgets_urls": null,
  "room_directory": { "servers": ["example.com"] },
  "setting_defaults": { "UIFeature.registration": false }
}
EOF
jq . /etc/element-web/config.json > /dev/null && echo "config.json is valid JSON"

Now the nginx site: Matrix API calls on matrix.example.com go to Synapse, and chat.example.com serves Element. Here we hit the one real snag of the afternoon. Element's README asks for security headers and for no-cache on index.html and config.json. Our first version set the security headers for the whole site and Cache-Control inside two location blocks, and curl -I showed the security headers missing from exactly those pages. In nginx, an add_header inside a location replaces every header set above it. The fix is a small file of headers included in each block:

# as root
cat > /etc/nginx/snippets/element-headers.conf <<'EOF'
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "frame-ancestors 'self'";
EOF

cat > /etc/nginx/sites-available/matrix <<'EOF'
# Synapse client API on matrix.example.com
server {
    listen 80;
    listen [::]:80;
    server_name matrix.example.com;
    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_http_version 1.1;
    }
}

# Element Web on its own hostname, chat.example.com
server {
    listen 80;
    listen [::]:80;
    server_name chat.example.com;
    root /usr/share/element-web;
    index index.html;

    include snippets/element-headers.conf;

    location = /index.html {
        include snippets/element-headers.conf;
        add_header Cache-Control "no-cache";
    }
    location = /config.json {
        include snippets/element-headers.conf;
        add_header Cache-Control "no-cache";
    }
}
EOF
ln -s /etc/nginx/sites-available/matrix /etc/nginx/sites-enabled/matrix
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
curl -s -I -H 'Host: chat.example.com' http://127.0.0.1/config.json

nginx -t should end with "test is successful". The curl answer should show 200 OK, the three security headers and Cache-Control: no-cache. client_max_body_size matches the 100 MB upload limit in Synapse, so big files are not cut off by nginx first.

14:50 The .well-known files on your main domain

These go on the server that answers for example.com itself. /.well-known/matrix/server tells other Matrix servers to connect to matrix.example.com on port 443. /.well-known/matrix/client is for apps: when Anna types @anna:example.com into Element on her phone, the app reads it to find the server, and the Access-Control-Allow-Origin header lets web clients read it. On an nginx website, add this site or copy the two location blocks into the existing one:

# as root, on the server that answers for example.com
cat > /etc/nginx/sites-available/wellknown <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    location = /.well-known/matrix/server {
        default_type application/json;
        return 200 '{"m.server": "matrix.example.com:443"}';
    }
    location = /.well-known/matrix/client {
        default_type application/json;
        add_header Access-Control-Allow-Origin *;
        return 200 '{"m.homeserver": {"base_url": "https://matrix.example.com"}}';
    }
}
EOF
ln -s /etc/nginx/sites-available/wellknown /etc/nginx/sites-enabled/wellknown
nginx -t && systemctl reload nginx
curl -s -H 'Host: example.com' http://127.0.0.1/.well-known/matrix/client

Expected output: {"m.homeserver": {"base_url": "https://matrix.example.com"}}. The server file only matters once federation is on, so it can wait, but it does no harm.

15:10 HTTPS, then log in

Element and the mobile apps only talk to a server over HTTPS, and browsers block calls and camera access on plain HTTP pages. Put certificates on matrix.example.com, chat.example.com and example.com with Certbot or a reverse proxy such as Caddy; our reverse proxy and SSL guide covers all three common tools. The firewall then only needs ports 80 and 443 open. Port 8448 is only for federation without delegation, so you can leave it closed.

Open https://chat.example.com, and the login page already knows your server. Create a space called after your company, then rooms inside it for each old Slack channel: #general, #sales, #support. Invite people by their Matrix ID.

End-to-end encryption, without the jargon

End-to-end encryption (E2EE) means messages are locked on the sender's device and unlocked only on the readers' devices. The server stores them, but cannot read them, and neither can anyone who steals a database backup. Matrix uses the Megolm scheme for rooms. In our lab we created a private room through the API with encryption switched on, and the room state read back {"algorithm":"m.megolm.v1.aes-sha2"}. Element does the same for new private rooms and direct messages on its own.

Three things surprise teams coming from Slack:

Federation: on or off?

With federation on, your colleagues can join public Matrix rooms and chat with people on other servers, including partners who run their own. With it off, your server is an island, like a Slack workspace without shared channels. For most companies "off" is the right start: no unknown servers send you traffic, and no message leaves your server.

We tested the switch. With federation_domain_whitelist: [], asking our server for a profile on matrix.org failed at once with {"errcode":"M_FORBIDDEN","error":"Federation denied with matrix.org."}, and joining #matrix:matrix.org returned "Failed to fetch alias". With the line deleted and Synapse restarted, the request went out to matrix.org instead (it then failed only because our lab name, example.com, is not a real server that matrix.org can verify). A middle way is to list only your partners' domains, for example federation_domain_whitelist: ["partner.com"].

Moving your Slack history, and the rate limit trap

The simplest path is to keep Slack's export as an archive and start fresh. If you want old messages inside Matrix, you need a bridge or an import script, and that is where Synapse's built-in protection bites. By default each user may send 0.2 messages per second with a burst of 10, and join 0.1 rooms per second (Synapse config docs). Our first test sent 1,000 messages from two accounts in a loop: only 176 were stored, and the other 824 got 429 Too Many Requests. Marco also joined only 10 of the 20 rooms he was invited to.

The fix is to lift the limit for the importing account with the admin API. Get an admin access token, then override the limit for that user:

# as root
ADMIN_TOKEN=$(jq -n --arg p "$(cat /root/admin.pw)" '{type:"m.login.password",identifier:{type:"m.id.user",user:"admin"},password:$p}' | curl -s -X POST http://localhost:8008/_matrix/client/v3/login -H 'Content-Type: application/json' -d @- | jq -r .access_token)

curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' -d '{"messages_per_second":0,"burst_count":0}' "http://localhost:8008/_synapse/admin/v1/users/@anna:example.com/override_ratelimit"

The answer echoes {"messages_per_second":0,"burst_count":0}. After that, the same 1,000 messages went through in 43 seconds with 1,000 answers of 200. Logins are limited too: after a handful of quick logins from one address we got M_LIMIT_EXCEEDED and a 43-second wait, so scripts should reuse one token. The open-source mautrix-slack bridge can link Slack and Matrix during a gradual move; we did not test it here.

Voice and video calls

Element Web includes Element Call for group calls, but your server needs two extra pieces for it: a LiveKit SFU (a media server that forwards the video streams) and Element's small MatrixRTC authorisation service, lk-jwt-service, which swaps a Matrix login for a LiveKit access token. Clients find them through an org.matrix.msc4143.rtc_foci entry in /.well-known/matrix/client. The Synapse settings for this have changed between releases, so follow that README for your version. We did not set up calls in this lab, because they need public HTTPS and open UDP ports. Video forwarding uses far more CPU and bandwidth than text, so plan a VPS Mini or larger for regular group calls.

How much server a Matrix team chat needs

Our measurements, from a Debian 13 container with a 3 GB memory limit, after creating 4 users, 21 rooms and 1,176 messages:

Measured in our labIdle after restartAfter the message test
Whole system, free -m "used"200 MB245 MB
Synapse92 MB107 MB
PostgreSQL, including its cache116 MB148 MB
nginx3 MB3 MB
DiskSynapse 278 MB, Element 138 MB, PostgreSQL 79 MBDatabase 24 MB

Service figures come from systemd's memory counters. A closed company server stays small because it only stores your own rooms; joining large public rooms over federation is what makes Synapse hungry. Our sizing estimate from these numbers (not a load test): a VPS Nano (1 vCPU, 1 GB RAM) runs a closed text chat for a small team, a VPS Micro (2 vCPU, 2 GB RAM, 40 GB NVMe) is the comfortable start for 10 to 50 people, a VPS Mini (4 vCPU, 4 GB RAM) fits federation, bridges or LiveKit calls, and a VDS Small (8 GB RAM, 240 GB NVMe) suits a few hundred users or heavy file sharing. You can move to a bigger plan later from the client area with a short reboot.

Day two: updates, leavers and backups

Updates come through apt, so apt update && apt upgrade keeps Synapse, Element and PostgreSQL current. When someone leaves, switch off their account with one call. In our lab this answered {"id_server_unbind_result":"success"}, and the admin user list then showed Marco as deactivated:

# as root, with ADMIN_TOKEN from the previous step
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' -d '{"erase": false}' "http://localhost:8008/_synapse/admin/v1/deactivate/@marco:example.com"

For backups you need three things: the PostgreSQL database, the media folder /var/lib/matrix-synapse/media, and /etc/matrix-synapse, which holds the server's signing key. Without that key, a restored server is a stranger to every other server it knew. Our guide to off-site backups with restic shows how to ship them to a second machine every night, and Uptime Kuma can warn you if chat goes down.

Frequently asked questions

Is Matrix with Element a good Slack alternative for a small company?

Yes, if you want your messages on your own server and no per-user fees. Element covers channels (rooms), groups of channels (spaces), threads, file sharing, direct messages and end-to-end encryption, with apps for the web, desktop, Android and iOS. Slack still has more ready-made app integrations.

How much RAM does a Matrix Synapse server need?

Less than most guides suggest for a closed team. In our lab, Synapse used 92 to 107 MB and the whole stack with PostgreSQL and nginx used 200 to 245 MB for a small team. Federation with large public rooms, bridges and video calls need much more, so plan 2 to 4 GB for those.

How do I disable federation in Synapse?

Set federation_domain_whitelist: [] in a file in /etc/matrix-synapse/conf.d/ and restart Synapse. The Synapse documentation calls an empty list the recommended way to disable federation. To allow only partners, list their domains instead.

Is Element end-to-end encrypted by default?

Element turns on end-to-end encryption for new direct messages and private rooms. Public rooms are not encrypted by default. Each user should save their recovery key, because it is needed to read old encrypted messages on a new device.

Can I import my Slack history into Matrix?

Not with a built-in button. Teams either keep the Slack export as an archive or use a bridge or import script. If you script an import, lift Synapse's message rate limit for the import account with the admin API, or most messages will be rejected with error 429.

Your chat, your server

A VPS, a list of names and an afternoon of work moved our test team from the Slack model to a chat server we own. RS Computers runs KVM VPS and VDS servers in Amsterdam, Dublin and Prishtina, all with NVMe storage, unmetered traffic and their own IPv4 and IPv6 addresses; availability by city is on the plans page. Pick a size on our VPS plans page, and if you would rather have us install Synapse and Element for you, message us on Telegram for a quote.

← All articles

Chat on Telegram