Running a Lean Matrix Stack: The Decisions That Actually Matter

Running a Lean Matrix Stack: The Decisions That Actually Matter
Photo by Anirudh / Unsplash

The stack is settled: Synapse running single-process with SQLite, Synapse Admin for visibility, Maubot for bots, Traefik handling HTTPS. No Postgres, no Redis, no workers. One docker-compose.yml. The rationale for that shape is clear enough — at personal homelab scale, the full dependency chain doesn't earn its keep. What comes next is harder: the decisions that determine whether this stack actually holds, and under what conditions it doesn't.

Three decision points shape this deployment more than any others. None of them are configuration trivia. Each one commits you to something in another direction, and each one answers differently depending on whether you're running a personal server or something with real operating exposure.

SQLite: Not Whether It Works, But When It Stops

The community consensus on SQLite for Synapse is blunt: don't. Official documentation calls it testing-only. Every guide that matters assumes Postgres. The recommended migration path — synapse_port_db — exists specifically because people start with SQLite and have to move.

The question worth asking isn't whether SQLite works for Synapse. It does. The question is what kind of load causes it to fail, and whether your deployment will generate that load.

SQLite's constraint is write concurrency. One writer at a time, file-level locking. For Synapse, the write volume that hits that ceiling isn't primarily local user activity — it's federation traffic. When a user on your server joins a large public room, Matrix delivers every event from every participant in that room to your local database. A room with thousands of members generates thousands of concurrent writes during busy periods. SQLite serializes them. Postgres handles them in parallel. At federation scale with active large rooms, that difference is structural.

At homelab scale with a closed server — no public federation, invited users only, modest room activity — the write profile stays well inside SQLite's window. Local users generating read receipts, small message volumes, and occasional media uploads don't create the concurrency that SQLite struggles with. The risk the community is warning about doesn't exist in that operating context.

For a homelab deployment, the real SQLite question is this: will your users join large public federated rooms? If the answer is no — if this is a private server for internal communication — SQLite is defensible and I'd stay there. If the answer is yes, or eventually, the calculus shifts. Federation membership brings external write volume you didn't design for, and SQLite will degrade before it fails, which means you may not notice the problem clearly until performance is already bad.

The enterprise version of this decision doesn't have the same latitude. A server with more than a handful of users, any federated room participation, or any expectation of uptime under load should start with Postgres. Not because the homelab reasoning is wrong, but because the consequence of getting it wrong is different. Discovering your database is a bottleneck in a personal setup means your messages are slow. Discovering it in a team environment means an incident and a migration under pressure. The operational overhead of running Postgres — another container, connection strings, backup strategy — is real but bounded. The overhead of migrating under load is harder to bound.

If you're starting SQLite and genuinely expect to stay small and closed, I'd start there and watch the metrics. Synapse exposes database timing in its logs. If you see database operations taking longer than they should, that's the signal, and you'll have time to act on it. If you're uncertain about the eventual scope — if "personal homelab" might become "small team" in six months — starting with Postgres now costs you an afternoon of setup and saves you a migration. That tradeoff is worth it if the uncertainty is real.

Federation: The Variable That Changes Everything Else

Federation is the decision that most directly changes the SQLite answer, but it's worth examining on its own terms because it changes more than the database calculus.

A federated Matrix server participates in the broader Matrix network. Users on your server can join public rooms, message users on other servers, and be discoverable by the wider ecosystem. A closed server handles only local users communicating with each other and explicitly invited contacts. The feature delta is significant; the operational delta is even more so.

Running closed means your server is your surface. The rooms exist, the users are known, the message volume is predictable. You control registration, you control room membership, and you have no exposure to traffic you didn't generate. For a homelab serving a household or a small trusted group, this is usually the right posture. The reasons to choose Matrix over simpler options — encrypted messaging, self-hosted data, programmable bots — don't require federation to be valuable. And the reasons not to federate at homelab scale are concrete: you take on external traffic you can't control, your server becomes findable, and the SQLite question reopens.

Enabling federation at homelab scale requires honest answers to a few questions. Do your users actually need to communicate with users on other Matrix servers? If the answer is no, the capability adds exposure without adding value. If the answer is yes — if there are specific contacts on other servers your users need to reach — you can federate selectively rather than openly. Limiting which servers you federate with, or only enabling directed DMs to known external users, keeps the surface manageable. Full public federation is a different posture entirely: your server participates in the global network, joins rooms with unknown membership, and your traffic profile becomes unpredictable.

In an enterprise deployment, federation decisions are often made by compliance requirements as much as by technical preference. A regulated environment may need to prohibit external federation entirely, not for performance reasons, but because message data can't leave the controlled infrastructure. An enterprise Matrix deployment that handles sensitive communications would typically run closed by policy. A deployment standing in for Slack for internal teams might permit federation selectively, with known partner organizations on known servers. The decision moves from "do we want this feature" to "what are we allowed to do."

For this homelab: closed unless there's a specific reason not to be. Easier to open later than to close down a server users have been federating with.

The Maubot Auth Path: One Decision You Can't Walk Back

The Maubot authentication decision looks like a configuration detail. It behaves like a trap.

Setting up a Maubot client — the bot account that Maubot operates as — requires an access token and a device ID. The way most people discover they need these is by looking at the Maubot Manager web interface, which asks for them. The natural next move is to log into the bot's Matrix account via Element, extract the token from the settings, and paste it in. This works, briefly. When the bot account logs out of Element — or when the session is cleared, or when you log in again to check something — the token is invalidated. Maubot stops working. The token it has is no longer valid. You have to get a new one and update the database.

The correct path is mbc auth, the Maubot CLI authentication command. It creates a dedicated device session for Maubot, records the device ID, and returns an access token that belongs to that session and only that session. As long as nothing else terminates that specific device session, the token holds. The key constraint is that the bot account's Element session and the Maubot device session are separate sessions — logging into Element as the bot account doesn't affect Maubot's session unless you explicitly revoke devices.

This matters because the consequences of using the wrong path are delayed and confusing. The bot works at setup, stops working later for reasons that aren't immediately obvious, and the fix requires going back into the database. It's a recoverable situation, but it's the kind of friction that makes you question whether the whole stack is stable when it's actually just one configuration choice that was made in the wrong order.

The homelab consequence is mostly time and irritation. The enterprise consequence, if bots are doing anything operational, is more visible: a bot that stops responding, a manual DB fix required, and an explanation of why it happened. The right path costs nothing extra and prevents the failure entirely. There's no reason to use the Element login path for Maubot setup; it's the path that exists because it's the obvious one, not because it's the right one.

One related decision: the Maubot config.yaml entry for the homeserver must match exactly what you use in mbc auth. Not the domain, not the server name — the string you registered in the config. A mismatch produces a "Registration target server not found" error that requires reading the config carefully to resolve. Define the homeserver entry first, use that exact string in auth, and you won't hit it.

What to Carry Into the Build

These three decisions — SQLite scope, federation posture, and Maubot auth path — share a pattern. Each one has an obvious default and a non-obvious correct answer. The SQLite default is Postgres because the documentation says so; the correct answer for this context is SQLite unless you federate into large rooms. The federation default is enabled because Matrix is a federated protocol; the correct answer for a personal server is closed unless you have specific reasons to open it. The Maubot auth default is Element login because that's how you log into Matrix accounts; the correct answer is mbc auth because it creates a session that holds.

In each case, the default was set for a different context. Recognising that and choosing deliberately — rather than following the guide and accepting the default — is what makes this stack hold at the scale it's designed for.

Ryan Mckernan
Author

Ryan Mckernan

IT service manager & infrastructure architect turning real-world IT messes into practical, documented fixes. I build systems, streamline ops, and share field notes at Painfully Useful—tested, refined, and repeatable.