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:
- Synapse 1.162.0 is the current stable release, published on 29 September 2026 (Synapse releases). It is free under the GNU AGPL, with a paid commercial licence offered by Element as an option.
- Element Web 1.12.30, also from 29 September 2026, is the browser client we installed from Element's own Debian repository (Element Web releases).
- Slack Pro costs $8.75 per user per month billed monthly or $7.25 billed yearly, Business+ costs $18 or $15, and the free plan keeps only 90 days of message history (slack.com/pricing).
- Microsoft Teams Essentials costs $4.00 per user per month on a yearly subscription, with up to 300 meeting participants and 10 GB of cloud storage per user (Microsoft).
- Slack's data residency is only available on Business+ and Enterprise, and member profiles and channel membership can still be stored outside your region (Slack help).
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:
| Time | Step | Result |
|---|---|---|
| 13:00 | Choose names and DNS | User IDs like @anna:example.com fixed for good |
| 13:20 | Install Synapse and PostgreSQL | 29 seconds of apt in our lab |
| 13:40 | Database, closed sign-up, federation off | Strangers get "Registration has been disabled" |
| 14:00 | Create everyone's account from a list | Four accounts, random passwords |
| 14:20 | Element Web and nginx | Branded client on chat.example.com |
| 14:50 | .well-known files on the main domain | Apps find the server from the email-like ID |
| 15:10 | HTTPS, rooms, encryption, history import | First 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:
| Homeserver | Language | Status on matrix.org | Notes |
|---|---|---|---|
| Synapse | Python | Stable | Reference server, Debian packages, full admin API. What we used. |
| Continuwuity | Rust | Stable | Community fork in the Conduit family, light on RAM |
| Tuwunel | Rust | Stable | Successor to conduwuit, which is now listed as obsolete |
| Conduit | Rust | Beta | The original small Rust server |
| Dendrite | Go | Beta | In 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:
| Option | Price per user | 25 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 | $0 | One 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:
- An A record (and AAAA for IPv6) for matrix.example.com pointing to your VPS. Synapse answers here.
- An A and AAAA record for chat.example.com pointing to the same VPS. Element Web lives here. Element's README says it does not recommend running Element on the same domain as the homeserver, because of cross-site scripting risk.
- Access to the web server of example.com, to add the two .well-known files.
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:
enable_registration: falsekeeps public sign-up closed. It is the default, but writing it down tells the next admin it was intended.registration_shared_secretlets you create accounts from the command line on the server.federation_domain_whitelist: []switches federation off. The Synapse docs call an empty list "the recommended way of disabling federation".max_upload_sizeraises the file limit from the default of 50 MB.
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:
- Each person must keep their recovery key, which unlocks old messages on a new laptop or phone. If someone loses both the key and all their logged-in devices, their old history stays locked. Save it in a password manager on day one.
- The server cannot search encrypted rooms, so search happens on the device, and Element in the browser searches encrypted history less well than Slack does.
- Encryption cannot be switched off in a room once it is on. Leave announcement rooms unencrypted if you want them searchable, and encrypt anything with personal data.
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 lab | Idle after restart | After the message test |
|---|---|---|
Whole system, free -m "used" | 200 MB | 245 MB |
| Synapse | 92 MB | 107 MB |
| PostgreSQL, including its cache | 116 MB | 148 MB |
| nginx | 3 MB | 3 MB |
| Disk | Synapse 278 MB, Element 138 MB, PostgreSQL 79 MB | Database 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.