Self-hosting has a reputation problem. Half the “self-hostable” chat tools out there mean a Kubernetes cluster, a message broker, a search cluster, an object store, and a weekend. Fluid Chat means Postgres and a docker compose up.
Everything your team writes lands in a database you control, in a datacentre you chose, under a backup policy you wrote. No vendor holds the export button.
What self-hosting actually requires
The full dependency list, with no asterisks:
- Postgres 14+ — the only hard dependency. Messages, channels, users and the full-text search index all live here. No separate search cluster.
- Node 20+ — if you're running from source rather than containers.
- S3-compatible object storage for uploads and workspace exports. MinIO on the same box counts.
- Redis — optional. You need it only once you run more than one app or realtime process, at which point realtime events fan out over pub/sub instead of a direct HTTP post.
- SMTP if you want invitation and notification email. The app runs without it; people just invite by link instead.
Installing it, start to finish
The container route is three commands and gets you a working workspace on localhost:3000. The first workspace you create makes you its owner, with #general and #random already there.
# clone, configure, rungit clone https://github.com/azianmike/fluid-chat.gitcd fluid-chat && cp .env.example .envdocker compose up app ready on http://localhost:3000 realtime websockets on :3001 worker jobs running postgres migrations appliedIf you'd rather run the processes directly — which is what you want on a box where Postgres already exists — it's the same thing without the compose file:
cp .env.example .envnpm installnpm run db:migratenpm run dev # app on :3000npm run realtime # websockets on :3001npm run worker # scheduled jobsDocs for SMTP, S3/MinIO, HTTPS and domains, backup, restore and upgrade all ship in the repo — start with the install guide.
What each process does
Four things run. Understanding them takes about ninety seconds, which is roughly the point.
- 1The app (Next.js 15) serves the client and the
/apisurface. The web client has no private endpoints — it calls the same API you can, and there's a generated OpenAPI 3.1 document to prove it. - 2Postgres stores everything, including the search index. Full-text search with real operators runs here, over your entire history, however long that is.
- 3The realtime server (Socket.IO) handles presence, typing indicators and event relay, with room-level authorization. If it falls over, writes still go over HTTP and the app keeps working — you lose live updates, not the ability to send messages.
- 4The worker handles scheduled sends, reminders, retention policy enforcement, exports and email.
Sizing: what hardware does this actually need?
Less than you'd think. Team chat is a low-throughput workload dressed up as a realtime one — a busy 50-person team generates a few thousand messages a day, which is nothing for Postgres.
| Team size | Reasonable starting point | Notes |
|---|---|---|
| Up to ~25 | 1 vCPU / 2 GB, Postgres on the same box | A $6–12/month VPS genuinely does this. |
| 25–100 | 2 vCPU / 4 GB, managed Postgres | Redis still optional if you run one app process. |
| 100–500 | 2+ app processes behind a load balancer | Add Redis so realtime events fan out across processes. |
| 500+ | Separate Postgres with real IOPS, several app processes | The database is the thing to scale first, as ever. |
Read that table next to a per-seat invoice. A hundred people on a mainstream paid chat plan runs into five figures a year; a hundred people on a $20/month box does not.
Backups, upgrades and the unglamorous parts
Self-hosting is mostly a promise to do four boring things. Fluid keeps that list short because the data model is one database.
- Backup is a Postgres dump plus your object store. That's it — there's no second datastore holding half your messages.
- Restore is the reverse, documented in the repo, and worth rehearsing once before you need it.
- Upgrades run migrations on start. The upgrade doc covers the ordering.
- HTTPS and domains get their own doc, because reverse-proxy config is where most self-hosting afternoons quietly disappear.
The security story when you own the box
Self-hosting moves the trust boundary rather than removing it. What Fluid does on its side of that boundary:
- Passwords hashed with Argon2.
- Every query workspace-scoped through permission helpers, not ad-hoc filters sprinkled through route handlers.
- Guests confined to the channels they're added to.
- API keys with explicit scopes, per-key rate limits, rotation, expiry, and audited creation.
- Audit log, retention policies and per-workspace read-only mode included rather than tiered.
- Nothing phones home. There is no telemetry endpoint to block.
The part you own: patching the host, keeping Postgres current, and terminating TLS properly. That's the deal, and it's the same deal every self-hosted tool offers — the difference is how much surface area you're agreeing to babysit.
Two minutes to a workspace on your own hardware.
Clone it, run docker compose up, and the first account you make owns the workspace. If you'd rather kick the tyres before touching a server, the hosted version is free too.
Who should not self-host
Self-hosting is a good idea for fewer teams than the internet implies. Skip it if nobody on your team wants to own a Postgres, if you have no backup story for anything else you run, or if your actual requirement is “stop paying per seat” rather than “control the data” — in which case the hosted version is free and does that already.
Self-host when data residency, network isolation, or plain institutional stubbornness makes it the right call. Those are all fine reasons. “Because it's cooler” is also a fine reason, honestly, but budget the weekend.