Self-hosting & licensing
Institflow ships as one codebase, two deployment modes, and one API contract — everything in this reference works identically either way.
Cloud vs. self-hosted
- Cloud — multi-tenant, hosted for you, billed by subscription. Each institute is one tenant, reachable at a subdomain or a verified custom domain.
- Self-hosted — a single-tenant Docker bundle (
api,worker,web,caddy, plus Postgres/Redis/MinIO) you run on your own server, gated by a signed license issued from your provider's cloud platform.
Self-hosted install
The short version — prerequisites: a Linux server reachable on 80/443, Docker Engine + the Compose plugin, a domain pointed at the server, and a license key.
-
Get a license. Your provider creates your tenant on their own platform and issues a license for it — you receive a
LICENSE_KEY, shown once. There is no verification key to fill in: the key your install checks that license against is built into the published images, so nobody — including you — can replace it. -
Fill in the environment. Copy
docker/self-hosted.env.exampletodocker/.envand fill in every value — secrets are generated withopenssl rand -base64 32, the JWT keypair with a one-timekeys:generatescript. -
Start the bundle.
bashdocker compose -f docker/compose.self-hosted.yml --env-file docker/.env up -d docker compose -f docker/compose.self-hosted.yml --env-file docker/.env psapi-initis a one-shot migration runner and is expected to exit0; every other service stays running. -
First login. A bootstrap platform admin (from your
.env) makes one API call to create your real tenant and invite its owner — that account has no further purpose afterward and is worth disabling once your tenant exists.
Updating is a version bump plus docker compose pull && up -d — api-init re-runs (idempotently) on every start and blocks the API from serving traffic until migrations finish.
License lifecycle
Subscriptions (cloud) and licenses (self-hosted) are set manually by your provider — there's no automatic billing. When the paid period ends, the system only warns; it never blocks access on its own.
- Valid — full functionality, nothing visible to end users.
- Expired — still fully functional, indefinitely. Every response carries an
X-Subscription-Warning: expired; since=<date>header, and the web app shows an owner/manager-only banner naming the date. Nothing else changes — an expired institute keeps full access until someone locks it. - Locked — the only state that blocks access, and it is always the result of an explicit, reason-carrying lock command from your provider — expiry alone never produces one. Tenant-scoped requests get
402 SUBSCRIPTION_SUSPENDED, except authentication, health checks, and the billing page itself. Data is never deleted or modified — unlocking restores full access immediately, exactly where you left off.
An optional platform-wide auto-lock policy can lock an expired tenant automatically after a configurable delay — off by default, so nothing is ever locked unless someone turns it on. A per-tenant override can also opt any single institute in or out of that policy, regardless of the platform-wide default.
A self-hosted install keeps working with no time limit while it can't reach the license server — being offline changes nothing on its own, no matter how many consecutive checks fail. An install reports tamper signals (a clock rolled back, a missing heartbeat, and similar) to your provider, but these are alert-only: nothing is ever locked automatically because a signal fired, only ever through the same explicit signed command a human issues. If an install is locked and the license server is unreachable, your provider can mint a one-time unlock token for you to paste into the self-hosted admin and restore access offline.
Security notes
- Change every secret in the example env file before going live — none of its blank values are safe defaults.
- TLS is automatic (Caddy) as long as your domain resolves and port 80 is reachable for the initial ACME challenge.
- Only 80/443 need to be internet-reachable — Postgres/Redis/MinIO/the API itself are never published to the host.
MASTER_KEYencrypts tenant-supplied provider credentials (WhatsApp/SMS/email API keys) at rest — treat it like a database password, and back it up. Losing it makes any saved credentials permanently undecryptable.
Back up more than the database
Also back up the ims_minio_data volume (uploaded files) and your .env (secrets) with the same discipline as your Postgres dump.