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.

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

  2. Fill in the environment. Copy docker/self-hosted.env.example to docker/.env and fill in every value — secrets are generated with openssl rand -base64 32, the JWT keypair with a one-time keys:generate script.

  3. Start the bundle.

    bash
    docker 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 ps

    api-init is a one-shot migration runner and is expected to exit 0; every other service stays running.

  4. 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 -dapi-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.

  1. Valid — full functionality, nothing visible to end users.
  2. 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.
  3. 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_KEY encrypts 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.