August 29, 20264 min read
A support inbox you can read the source of
Why the core is AGPL-3.0, what self-hosting actually looks like, and where the open part ends.
By fikadesk
A support inbox holds the mail your customers trusted you with. Names, addresses, order numbers, the complaint someone typed while their card was being declined. If the company that runs your inbox disappears, or pivots, or starts reading the messages, you explain it to your customers. That is why the core of fikadesk is AGPL-3.0.
The license is not the whole story. AGPL asks that if you run a modified copy and serve it to others over a network, you publish your changes. A hyperscaler cannot resell our server unmodified under a new name, and you can still read every line that touches your data. The parts that run inside your customers' sites, the widget, the protocol package, the API clients, are Apache-2.0, because embedding them in a proprietary site should not drag that site into AGPL.
What lives where
The AGPL core is the api, the worker, the scheduler, the mail ingest and the compose stack. The Apache-2.0 parts are the messenger widget, packages/protocol, the SDKs and API clients, and the MCP server. The open core ends at ee/, which holds SSO/SAML, SCIM and audit export under a commercial license, gated by a key. The core never imports ee/, so the open-source image builds with that directory deleted.
We also wrote down what will never move into ee/. SLAs, custom roles, agent capacity, audit logs, every channel, and the AI agent with a key you bring yourself stay in the AGPL core. Those are the features a competitor called Chatwoot locks behind enterprise. If your team is choosing a support tool on the question of what you lose when you do not pay, the answer here is SSO, SCIM, residency and support, not the inbox itself.
The last two years of open-source infrastructure made this an easy call. Redis relicensed, MinIO archived its repository, Cal.com moved production code behind a closed door. AGPL is the only license that stops a reseller without punishing a self-hoster.
Self-hosting is one compose file
Everything runs from deploy/compose/. The single profile starts Caddy, Postgres 18, PgBouncer, Centrifugo, a one-shot migration job, the api, the worker and the mail ingest. There is one application image. FIKADESK_ROLE picks which process a container runs, from api, worker, scheduler, ingest or migrate.
cp deploy/compose/.env.example deploy/compose/.env
openssl rand -hex 24 # POSTGRES_PASSWORD, FIKADESK_APP_PASSWORD
openssl rand -hex 32 # FIKADESK_AUTH_SECRET, FIKADESK_REALTIME_SECRET
$EDITOR deploy/compose/.env
docker compose -f deploy/compose/compose.yml --profile single up -d --wait
curl http://127.0.0.1/healthzThe first up builds a derived Postgres image, because pgBackRest comes from the PostgreSQL APT repository, so it needs network access and a minute or two. Later starts reuse it. depends_on fixes the order: Postgres becomes healthy, a roles job creates the two app roles and sets the app password, migrate applies the migrations and installs the pg-boss schema, then api, worker and ingest start once Centrifugo is healthy, and Caddy comes up last.
Migrations run before the code, never at boot
Migrations are hand-written SQL, additive only, and they run as a pre-deploy step. To upgrade you bump FIKADESK_IMAGE, pull, run the migrate container, then recreate the rest.
$EDITOR deploy/compose/.env # bump FIKADESK_IMAGE to the new tag
docker compose -f deploy/compose/compose.yml --profile single pull
docker compose -f deploy/compose/compose.yml --profile single run --rm migrate
docker compose -f deploy/compose/compose.yml --profile single up -d --waitrun --rm migrate fails loudly if a migration cannot apply, and the old api keeps running until you recreate it. The api also refuses to start against a database behind the migration version its build expects, so a half-applied upgrade is a failed boot, not a broken inbox. The runner is about two hundred lines and takes a session advisory lock, so two migrators cannot step on each other.
Email needs no account anywhere. Outbound is SMTP submission to a relay you already have. Inbound arrives from the MTA on the same box. The DKIM keys are generated and held on your machine, not in our cloud. Set FIKADESK_DOMAIN and FIKADESK_BIND to go public, and Caddy fetches a certificate and redirects HTTP to HTTPS.
The box is locked down
Every container drops all capabilities except Caddy, which keeps NET_BIND_SERVICE because its binary needs it, and runs no-new-privileges with a read-only root filesystem. The app image runs as uid 10001, not root. The api connects to Postgres as fikadesk_app, which runs under row-level security; cross-tenant work goes through an explicit SET LOCAL ROLE. Only the migrate job holds the owner password.
The honest limit
Attachments are the one feature this stack cannot run yet. Files a customer sends must be served from a second origin, so one of them cannot run scripts against the session that holds the inbox, and that storage adapter is still being built. Until it ships, the self-host stack is complete for text but not for files. We say so on the page instead of burying it, because you should find it before your first customer does.
Read the source, run it, fork it. If you serve a modified copy to others, AGPL asks you to publish the changes. That is the deal, and we are fine with it.