Blog

How Kommio's self-hosted edition is built: one binary, one database, and an upgrade you can rehearse

The self-hosted edition is the same product as Kommio Cloud, packaged for your own machines: what you run, why it is one process and one Postgres, how a release is cut and checked, what an operator does on upgrade day, how the built-in backups work, and how to get access while the edition is not public.

Kommio team · · Updated

Kommio Cloud is free and most teams start there. Some teams cannot: the data has to stay on their own machines, or the support desk must live inside a network Kommio Cloud cannot reach. For them there is the self-hosted edition, the same product as one process and one Postgres database.

The edition is not public yet. The releases exist, we run them, and teams get access by writing to [email protected]; the install guide and the configuration reference come with that access. This post is the part we can say to everyone: how the edition is designed, so you can judge whether it fits your infrastructure before you ask.

What you run

Two things: the Kommio binary and a Postgres database.

The binary serves the API and the apps, works the background jobs and runs the scheduler, all in one process by default. There is no Redis, no separate queue, no sidecar for websockets. Everything the processes have to agree on, the job queue, the event outbox, the schedules, who is online, goes through Postgres. When one server is no longer enough, the same binary runs with a role each (API, worker, scheduler) in separate processes, and the API can run as several replicas behind any proxy that health-checks it.

Postgres 18 is what we test and run, with the pgvector extension so the knowledge base can search by meaning; without the extension it searches by keywords. The files people send are stored on the server’s own disk, on a mounted share or disk (NFS, SMB, a cloud block volume), or in any S3-compatible bucket, and a built-in copy command moves them between any two of those without touching the database, so you can start on local disk and move to a share later.

The binary listens on localhost and expects a TLS proxy in front. Caddy is the shortest route, since it fetches its own certificate. You tell Kommio which proxy addresses to trust, so rate limits and the audit trail see real client addresses rather than the proxy’s; by default it trusts nobody.

The edition is released as a container image for amd64 and arm64, as static Linux binaries with checksums, and as a package holding a Docker Compose file, the configuration template and the operator guides. The Compose file is the common case: Postgres, Kommio, and a third service for backups that waits until you turn it on.

Why one binary and one database

Because every extra moving part is something an operator has to run, watch, back up and upgrade. A support desk that needs a web server, a job runner, a cache and a message broker to answer one email has four ways to be half-broken. Kommio has one process and one database, and the database is the one thing you were going to back up anyway.

The design holds under more than one process too. Jobs and the event outbox lock their rows, so a job runs once whichever process picks it up. Periodic schedules are leader-elected among the workers, so they fire once however many run. Live updates, a new message or a typing indicator pushed to an agent’s browser, are announced on a Postgres notification channel and every API process replays them into its own connections, so a browser gets its frames whichever replica it is connected to. None of that needs anything outside Postgres, and all of it is what makes the upgrade routine below safe.

Configuration that refuses to be wrong

Every setting is checked when Kommio starts. A wrong value stops the start and the log names the setting; there is no half-configured install serving requests.

Two settings matter more than the rest. The encryption key seals every stored secret: mailbox passwords, OAuth tokens, webhook and two-factor secrets. It is never part of a backup, so a copy belongs in your password manager, apart from the backups, because a restore without it leaves those rows unreadable and there is no recovery. The base URL goes into every link, email, OAuth redirect and cookie, so it is set to the final https name before the first start, not to a server’s address.

Before the first start, and before every upgrade, the binary has a preflight command. It checks the configuration, the encryption key, the database connection and version, pending migrations, the file store (it writes and deletes a probe), outbound mail, the base URL and the rest, and exits non-zero on anything that would make the start fail. A blocking failure changes nothing: fix what it names and run it again. We run the same command in our own deploys and in CI.

How a release is made

Releases are semantic versions, cut on demand when something is worth shipping to self-hosters, never on a schedule. Kommio Cloud does not use them; it deploys every change that passes CI. A release can only be cut from a commit that passed everything: the Go and web suites, the browser flows against a built server, the widget browser matrix, the load gate, a vulnerability scan of the module and of the container image, and the upgrade rehearsal.

The rehearsal is the part that matters on upgrade day. For every push, and from each of the newest two releases, CI seeds a database with the older build, runs preflight, migrates with the new build, and then proves two things: that the new build answers, signs in, lists conversations and rebuilds the reports a migration asked for without anyone starting it; and that the old build still runs on the migrated schema, next to the new one, and that its own migrate step changes nothing.

That second check is a rule made into a job: migrations are additive. A release adds tables, nullable columns or columns with defaults, and indexes; it never drops or renames what the previous release reads or writes. A migration that cannot keep the rule says so on a line of its own, CI skips the check for that one, and the release notes say to stop the old version before starting the new one. Those are rare.

Each release’s notes list the decisions and the migrations since the previous one, and call out what needs attention: a migration that builds indexes on large tables, one that rebuilds reports in the background, a breaking one. Nothing is announced and nothing phones home; there is no update check in the product.

What the operator does on upgrade day

Any release upgrades directly to any later one of the same major version, with no stops in between. The routine is short, and it is the same routine for the first install and the hundredth upgrade:

  1. Read the notes of the release you are moving to, and of any you skip over.
  2. Back up. Migrations are forward-only, so going back to the previous version is a restore, not a downgrade. With the built-in backups on, one command makes a backup now.
  3. Preflight with the new version against the running database. It lists the pending migrations and blocks on anything that would make the start fail.
  4. Start the new version. Migrations run before the process serves, so there is nothing to apply by hand, now or ever.

With one process, the install is down from the stop until the new version reports “api listening”. Most migrations take a second or two. An index build on a table with millions of rows runs concurrently and takes minutes, longer on slow disks; the notes flag those. A report rebuild runs after the start, in the background, at the lowest priority, and the reports keep answering from the old numbers meanwhile.

If a migration fails, it rolls back and the process exits with the error in its log; fix the cause and start again, and the applied migrations are skipped. To serve while you investigate, start the previous version: it runs on the partly migrated schema, because migrations are additive. That is not a downgrade, the schema stays where it is; to go back for real, restore the backup from step 2 into an empty database.

If seconds of downtime matter, an install with two replicas behind a proxy upgrades without any: migrate once with the new version while the old replicas keep serving, start a new replica and wait until it reports ready with the new version, stop one old replica, start the second new one, stop the last old one. A stopping process reports itself not ready first and keeps serving for a drain period, so the proxy has taken it out before in-flight requests finish. That is the routine Kommio Cloud runs on every deploy, and the post about it explains what the product had to guarantee for it to be safe.

Backups, built in and off by default

Kommio keeps state in two places, Postgres and the file store, and the built-in backup writes both to a target of your choice: a directory (a mounted disk, an NFS or SMB share, a cloud block volume) or an S3-compatible bucket.

Backups are off until you turn them on, on purpose. A backup written to a volume next to the database’s disk dies with it, and we would rather you choose where backups go than believe you are covered when you are not. Turning them on is a switch, a target, and ideally an encryption key.

Each run streams a pg_dump in custom format, one consistent snapshot, to the target, encrypted with age when you give it a public key, together with a manifest recording the time, the Kommio version, the newest migration, the size and the checksum. Writes are atomic: a directory target writes a partial file and renames it, a bucket target uploads in parts and the object appears only when complete, so no half backup is ever visible. With files on a directory store, each run also copies the files the target does not have yet; keys are content hashes, so a file is copied once. The default is one backup a day with the newest fourteen kept, and older dumps go only after a new one succeeded.

The age identity that decrypts the backups stays offline, in the password manager, never on the server or with the backups: whoever has it can read every backup, and without it none can be read. A fetched backup is verified against its manifest and every encrypted chunk is authenticated, so a damaged or altered backup fails to decrypt instead of restoring garbage.

The backup process reads only the database and file-store settings, not the server’s encryption key or mail credentials, so it runs as a service next to the app, from cron, or as a Kubernetes CronJob. Preflight warns when the last successful backup is older than two schedule intervals, and the platform console shows the same: up to date, last run failed, or overdue, with the last ten runs. The operator guide has the restore in six steps, into an empty database next to the current one; we ask every operator to run it once into a scratch database after setting backups up.

Watching it

Two endpoints say whether the process and its database are up, one for liveness and one for readiness, and the readiness one is what a proxy health-checks. Workspace administrators have Settings › Health in the app: inbox sync state, webhook deliveries, paused bots, automation failures, the backlog and who is online. Platform administrators have a console across every workspace with the job backlog, dead letters, failing webhooks and the backup state. Set an OpenTelemetry endpoint and every request, job and event exports traces and metrics to whatever you already run.

Getting access

The self-hosted edition is not public while the product is young. If your team needs Kommio on its own infrastructure, write to [email protected] with where it would run (a VM, Kubernetes, a private network), roughly how many agents and which channels, and whether you are moving from Chatwoot. We agree access to the releases and the guides with you, and the first install is short: fill the few required settings, run preflight, start, create the first workspace. Questions before and after go to the same address.