Self-hosted software: editions, licensing and installs

How one codebase ships both as a hosted service and as software a company runs itself: SaaS, self-hosted and bring-your-own-database compared, the edition decided at build time, offline licences as ES256-signed JWTs with activation by file exchange, Docker Compose and Helm packaging, air-gapped installs with signed images and SBOMs, key management and backups, and why nothing calls home.

In short

Most software today is SaaS: the vendor runs it and customers open a website. Some companies - banks, public bodies, defence suppliers - must keep every byte on their own servers, so they need self-hosted software they install and run themselves. {{brand}} ships both from one codebase: the hosted service, and a Private edition a company installs with Docker Compose or a Helm chart. Instead of payments, a Private installation reads a signed licence it checks offline, and it never calls home.

After this lesson you can Compare SaaS, self-hosted and bring-your-own-database delivery and say when each fits; design a licence that is a signed JWT checked offline, with activation by file exchange; package an application for one host and for Kubernetes, and ship it into a network with no internet using signed images and an SBOM; and plan the keys and backups a self-hosted product needs.

Words you need

SaaS, self-hosted and bring-your-own-database

Three ways to deliver the same product differ in one question: who runs it, and where the data lives. The answer decides who upgrades, who backs up, who can see the data, how the vendor is paid and how fast a customer can start. The middle way, bring-your-own-database, keeps the software with the vendor but puts one customer's records in a database the customer owns.

Bring-your-own-database splits the system into a control plane and a data plane. The control plane - accounts and sign-in, the organization directory, plans, billing - stays in the vendor's database, because one person can belong to several organizations. The data plane - one organization's own records - is routed to that organization's database for every request made inside it.

How a licence works in one picture

A self-hosted product cannot ask a server whether it may run, so the permission travels with the product as a signed file. Five steps cover the whole idea. The sections after the options go deeper into each one.

Deciding the edition at build time

Most products switch behaviour with runtime configuration: a setting, an environment variable, a feature flag. That is right for choices the operator owns. It is wrong for what the operator must not choose. If a self-hosted copy took its edition from a setting, whoever runs it could change one line, run it as the hosted service and skip the licence entirely. So {{brand}} makes the edition a fact of the build: an image compiled with a licence public key is the Private edition, whatever its settings say. The hosted image is built without one.

However the value gets into the build, the rule for the code is the same: read it in one place, and add it to the configuration last, so that no setting, environment variable or command-line value can override it.

Compiled is not tamper-proof Someone who patches the binary can remove any check - no scheme survives a determined owner of the machine. A build-time fact stops the one-line bypass and keeps the honest path the easy one; the licence agreement covers the rest. Do not spend months on hidden checks that make support and stack traces worse for honest customers.

The options in the market

What {{brand}} chose, and why

The companies that need a Private edition are the ones with the strictest rules, and some of them have no internet at all. That one fact decided almost everything below.

An online lease - a short signed lease renewed daily, with a grace period when the vendor cannot be reached - stays designed for later. It would need a hosted signing service with its own key and someone to run it: a running cost and an attack surface no customer has asked for yet.

When another choice is right Selling only to connected customers, with per-seat billing that must be exact? An online lease is fine. A desktop tool for individuals? A commercial licensing service saves months. One very large customer with its own platform team? A Kubernetes operator may pay for itself.

Digital signatures and ES256, from the inside

A licence must be something the customer can read but not change, and that the installation can check without asking anyone. That is exactly what an asymmetric signature gives. The vendor signs with a private key that never leaves its vault; every installation holds only the public key, which can check a signature but never make one.

ECDSA needs a good random number Signing two messages with the same random number reveals the private key - the mistake that once exposed a games console's signing key. Use your platform's crypto library, which handles this (or derives the number deterministically, as RFC 6979 describes), and never write ECDSA yourself.

A licence as a signed JWT

A licence file is a JWT whose payload says who may run what, and for how long. Because the signature covers every byte, changing the seat count or the expiry date breaks it, so the file is safe to email, paste into a form or print. The type header marks it as a licence, so a different kind of signed file - such as an activation file - can never be taken for one. The field names below are made up for teaching: your own format will differ, and the ideas are what matter.

A licence cannot be recalled If a private key leaks, only a new build that stops trusting its key id helps, and every licence signed with it must be issued again. That is why the key id is in the header and the private key lives in a vault.

What a licence means over time

Verifying says a licence is genuine. A second, pure function says what it means now. Keeping the two apart makes both easy to test: the verifier needs keys, the rules need only dates. The rules run on every check, so an expiry takes effect on time and nothing has to be scheduled.

Read-only, never locked out A licence that ends must never destroy data or lock out the person who can fix it. A smaller licence should never remove anyone who already works - it only refuses new paying members.

Activation by file exchange

An unbound licence could run on any number of installations. Activation binds it to one. Online products do this with a call to a licence server; an air-gapped site cannot make that call, so the same exchange happens with two small files that a person carries across.

Counting seats with no licence server

A seat is one person with a paying role, counted once across all organizations of the installation. With no server to report to, the installation must enforce the number itself. Every path that can add a paying person has to go through the same check. Decide too what a person without a seat may still do, and say it plainly in the product.

Count under one lock Two administrators in two organizations can both take the last seat at the same moment. Take one installation-wide lock before any organization's own lock - always in the same order, so two requests never deadlock - and count inside it. In PostgreSQL an advisory lock (a lock on a number you choose, not on a row) does this across several servers. A wait that runs too long should refuse the change rather than let it through unchecked.

Packaging: one host with Compose, a cluster with Helm

Both packages run the same two images - the API, with its database migrator inside, and the web app on nginx - next to the customer's PostgreSQL. Shipping the migrator in the API's image means the code and the schema it expects can never come from different commits. Every secret reaches the containers as an environment variable from a file or a Kubernetes Secret - never baked into an image or written into a values file.

Refuse impossible values when the chart renders Helm templates can stop an install with a clear message, for example a fail call that says several replicas need S3 storage. A configuration that cannot work should fail before anything is created and name the value to change - not crash an hour later.

Health probes, proxies and the first administrator

Packaging settles three more things every self-hosted service meets: how the orchestrator asks whether it is healthy, who may reach it, and who the first administrator is.

Air-gapped installs, signed images and SBOMs

In an air-gapped network nothing can be pulled: no registry, no package mirror, no licence server. Everything arrives as files, so the customer must be able to prove those files are exactly what the vendor built. Three techniques do that together: pinning by digest, signatures with cosign, and a bill of materials.

Trust the key you hold, not the one in the box An archive may carry a copy of the public key and a verify script for convenience. Neither proves anything: whoever altered the archive could replace both. Verify with the key you received separately, when you became a customer, and run your own copy of any script.

Key management and backups for self-hosted software

A self-hosted product stores secrets it must use but never show again: AI provider keys, saved database connections, webhook secrets. {{brand}} encrypts them with ASP.NET Core Data Protection. It keeps a key ring: a set of keys, each valid for a period, with older keys kept to decrypt what they protected. Where the ring lives decides what a backup must contain - and who could read the secrets if a backup leaked.

Never just swap the certificate file The server would start happily - and every secret stored under the old keys would be unreadable. Name the new certificate as current and keep the old one listed as previous.

No telemetry home

Customers choose self-hosting so that nothing leaves. A product that quietly sends usage statistics, crash dumps or an update check breaks that promise - and in an air-gapped network every such call is a timeout waiting to happen. So a Private installation makes no call to the vendor by default: no licence check, no heartbeat, no usage report, no update check.

The one opt-in exception Error reports are off by default. An operator may keep them locally, or also send them. When sending is on, each report is shown to administrators for a short time first and any of them can stop it. It holds only the version, the error type, the route template, a correlation id and the product's own code frames - never a message, a value, a name or a file.

When the operator connects their own collector, telemetry stays clean too: requests are recorded by their route template (/cards/{id}), never by raw path or query string; outgoing calls by scheme and host only; and people by id, never by name or address. A token that sits in a URL never reaches anyone's logs.

Try it yourself: issue and verify your own licence

In twenty minutes, with Node.js 20 or later and no extra packages, you can build the heart of an offline licence system: a key pair, a signed licence, and a verifier that refuses forgeries.

Common mistakes

Interview questions

Related