Self-hosted

A self-hosted Slack alternative that fits on one small server.

Postgres is the only hard dependency. Docker Compose gets you a running workspace in about two minutes, and every message your team writes stays in a database you control.

No licence key, no seat count, no phone-home. Just your box and your data.

1
hard dependency: Postgres
~2 min
to a running workspace
4
processes, total
0
telemetry endpoints

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.

Everything in containers
# 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 applied

If 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:

Or from source
cp .env.example .envnpm installnpm run db:migratenpm run dev        # app on :3000npm run realtime   # websockets on :3001npm run worker     # scheduled jobs

Docs 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.

  1. 1The app (Next.js 15) serves the client and the /api surface. 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.
  2. 2Postgres stores everything, including the search index. Full-text search with real operators runs here, over your entire history, however long that is.
  3. 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.
  4. 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 sizeReasonable starting pointNotes
Up to ~251 vCPU / 2 GB, Postgres on the same boxA $6–12/month VPS genuinely does this.
25–1002 vCPU / 4 GB, managed PostgresRedis still optional if you run one app process.
100–5002+ app processes behind a load balancerAdd Redis so realtime events fan out across processes.
500+Separate Postgres with real IOPS, several app processesThe 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.

FAQ

Self-hosting, without the hand-waving.

What do I need to self-host Fluid Chat?

Node 20+, Postgres 14+, and S3-compatible object storage for uploads and workspace exports. Redis is optional and only becomes necessary once you run more than one app or realtime process. SMTP is optional if you invite people by link. Docker Compose bundles all of it if you'd rather not assemble the pieces.

How long does installation take?

Roughly two minutes on the container path: clone the repo, copy .env.example to .env, run docker compose up. The first workspace you create makes you its owner, with #general and #random already set up.

What server size do I need?

A 1 vCPU / 2 GB VPS comfortably handles a team of about 25 with Postgres on the same box. Around 25–100 people, 2 vCPU / 4 GB and managed Postgres is a sensible shape. Past a few hundred, run multiple app processes with Redis so realtime events fan out, and give Postgres real disk.

Does self-hosted Fluid Chat cost anything?

No licence fee, no seat count and no licence key to check — you pay your hosting provider and nobody else. The seat-limit and billing fields in the schema exist for anyone who wants to run Fluid Chat as a hosted service; they are inert in a self-hosted install.

Does my data ever leave my server?

No. Self-hosted, messages, files and the search index live in your Postgres and your object storage. There is no telemetry endpoint and nothing phones home, which you can confirm by reading the source rather than taking our word for it.

What happens if the realtime server goes down?

The app keeps working. Writes go over HTTP and realtime is treated as an accelerator rather than a dependency — you lose live updates and typing indicators, not the ability to send and read messages.

How do backups work?

A Postgres dump plus your object store covers everything, because there is no second datastore holding half the data. Backup, restore and upgrade each have their own doc in the repo. Rehearse a restore once before you need one.

Something not covered? Open an issue.

Your server. Your Postgres. Your messages.

Clone the repo and run it, or start free on the hosted version and move it in-house later. The code is the same either way.