Clave is a self-hosted license server. It issues device-bound license and trial activations as signed JWTs, supports offline validation and update delivery, and includes an admin dashboard and customer self-service portal.
- Backend — Go API and PostgreSQL migrations in
backend/. - Website — static admin and self-service UI in
website/. - Emailer — RabbitMQ consumer and SMTP sender in
emailer/. - Client integration — activation, validation, updates, grace periods, and
certificate pinning are documented in
CLIENT.md. - Server deployment — the supported Docker Compose and TLS-proxy procedure
is in
setup.md. - Version adoption — authorized client synchronization records the running version and optional platform diagnostics for the admin dashboard.
Generate the required signing key and application secrets:
go run ./scripts/generate_jwt_keys.go ./license-jwt-private.pem
openssl rand -hex 32 # LICENSE_HMAC_SECRET
openssl rand -hex 32 # SELF_SERVICE_TOKEN_PEPPER
openssl rand -hex 32 # CSRF_AUTH_KEY
openssl rand -hex 32 # ADMIN_MFA_CODE_PEPPERCreate .env, set LICENSE_JWT_PRIVATE_KEY_FILE to the absolute path of the
generated PEM, add the other values, and start the development infrastructure:
docker compose -f compose.dev.yaml up -dThe API is at http://localhost:8000, PostgreSQL at localhost:54321, and
Mailpit at http://localhost:8025. The Compose stack includes the emailer worker,
so forced-development admin 2FA codes arrive in Mailpit. Start the UI with cd website && pnpm install && pnpm dev; Vite proxies /api to the backend.
The production topology is a TLS reverse proxy in front of two loopback-only
ports: the static website on 127.0.0.1:8080 and the API on
127.0.0.1:8000. PostgreSQL and RabbitMQ stay on the private Compose network.
The supplied compose.production.yaml implements
that layout.
Use a supported Linux server with a public DNS name. Point its A/AAAA record
at the server, allow inbound TCP 80 and 443, and install Docker Engine with the
Compose plugin. Install a TLS reverse proxy such as Caddy on the host. Do not
open ports 8000, 8080, 5432, or 5672 in the firewall.
Create a dedicated location and fetch the repository:
sudo install -d -o "$USER" -g "$USER" /opt/clave
git clone https://github.com/cheetahbyte/clave.git /opt/clave/app
cd /opt/clave/appFor repeatable deployments, check out a release tag or a reviewed commit rather than deploying a moving branch.
Create the signing key outside the repository and restrict its permissions:
install -d -m 700 /opt/clave/secrets
go run ./scripts/generate_jwt_keys.go /opt/clave/secrets/license-jwt-private.pem
cp .env.production.example .env.production
chmod 600 .env.production /opt/clave/secrets/license-jwt-private.pemIf Go is not installed on the server, run the generator on a trusted workstation and securely copy the resulting PEM to that path. The file is an Ed25519 PKCS#8 private key. Back it up: replacing it invalidates the signatures of existing activation tokens.
Generate independent values and place them in .env.production:
openssl rand -hex 32 # POSTGRES_PASSWORD
openssl rand -hex 32 # RABBITMQ_PASSWORD
openssl rand -hex 32 # LICENSE_HMAC_SECRET
openssl rand -hex 32 # SELF_SERVICE_TOKEN_PEPPER
openssl rand -hex 32 # CSRF_AUTH_KEY (must be exactly 32 bytes / 64 hex chars)
openssl rand -hex 32 # ADMIN_MFA_CODE_PEPPER (same format)Set PUBLIC_APP_URL to the public HTTPS origin, configure the emailer's
SMTP_URL and EMAIL_FROM, and leave DEV unset. RabbitMQ is required because
admin 2FA and other transactional email are delivered by the emailer worker. The two Compose passwords should be
URL-safe; the hexadecimal values above are. Never commit .env.production or
the PEM.
Build locally on the server, start the dependencies, then start the application:
docker compose --env-file .env.production -f compose.production.yaml build
docker compose --env-file .env.production -f compose.production.yaml up -d postgres rabbitmq
docker compose --env-file .env.production -f compose.production.yaml up -d backend website emailer
docker compose --env-file .env.production -f compose.production.yaml psRUN_MIGRATIONS=true applies all pending migrations before the API starts. Only
one backend instance should perform migrations during a deployment. The Compose
file persists PostgreSQL, RabbitMQ, and uploaded update artifacts in named
volumes and mounts the signing key read-only.
The backend serves HTTP and must not be exposed directly to the internet. This Caddy configuration serves the UI and routes same-origin API traffic to the backend; replace the hostname:
clave.example.com {
handle /api/* {
reverse_proxy 127.0.0.1:8000
}
handle {
reverse_proxy 127.0.0.1:8080
}
}Reload Caddy after installing the configuration. Keep
TRUST_PROXY_HEADERS=true: Caddy supplies the original scheme, and Clave then
enforces HTTPS, secure cookies, HSTS, redirects plaintext GET/HEAD requests,
and rejects plaintext requests with bodies. Only enable this setting behind a
trusted proxy that overwrites X-Forwarded-Proto, X-Forwarded-For, and
X-Real-IP.
The website uses a relative /api URL by default, so this same-origin layout
needs no frontend build variable and avoids cross-origin cookie problems.
Verify both layers before creating an account:
curl --fail https://clave.example.com/healthz
curl --fail https://clave.example.com/api/v1/health
docker compose --env-file .env.production -f compose.production.yaml logs --tail=100 backend emailerCreate the first administrator inside the backend container. Quote the password so the shell does not interpret punctuation:
docker compose --env-file .env.production -f compose.production.yaml exec \
backend /createadmin admin@example.com 'replace-with-a-strong-password'Sign in at https://clave.example.com/login and complete two-factor setup.
Back up all of the following together:
- PostgreSQL (
pg_dumpor a volume/storage snapshot). - The
update-artifactsvolume if Clave hosts update binaries locally. /opt/clave/secrets/license-jwt-private.pemand.env.productionin an encrypted secret store.
RabbitMQ contains pending email events; backing it up is optional but prevents queued messages from being lost. Test restores periodically. A database restore without the original signing key and application secrets is incomplete.
To update, back up first, check out the desired tag/commit, rebuild, and recreate the services:
git fetch --tags
git checkout <release-tag-or-commit>
docker compose --env-file .env.production -f compose.production.yaml build --pull
docker compose --env-file .env.production -f compose.production.yaml up -d
curl --fail https://clave.example.com/api/v1/healthInspect failures with docker compose --env-file .env.production -f compose.production.yaml logs <service>. Admin 2FA codes are published through
RabbitMQ and sent by the emailer, so both services and its SMTP configuration
must be working for anyone to sign in.
Values described as “truthy” accept true, 1, or yes (case-insensitive).
Unless noted otherwise, variables are read at process startup.
| Variable | Required/default | Purpose |
|---|---|---|
DATABASE_URL |
Default postgres://clave@localhost:54321/clave?sslmode=disable |
PostgreSQL DSN. Always set an authenticated production value. |
DATABASE_MAX_CONNS |
20 |
Maximum PostgreSQL connections used by the API pool. Must be greater than zero. |
LICENSE_JWT_PRIVATE_KEY_FILE |
Required | Path to exactly one PEM-encoded Ed25519 PKCS#8 private key. |
LICENSE_HMAC_SECRET |
Required | Secret used when deriving license keys. |
SELF_SERVICE_TOKEN_PEPPER |
Required | Secret mixed into self-service and organization tokens. |
CSRF_AUTH_KEY |
Required in production | Exactly 32 random bytes encoded as 64 hex characters; protects CSRF tokens. Dev mode otherwise generates an ephemeral key. |
ADMIN_MFA_CODE_PEPPER |
Required in production | Exactly 32 random bytes encoded as 64 hex characters; peppers hashed admin 2FA email codes. Dev mode otherwise generates an ephemeral key. The legacy name ADMIN_TOTP_ENCRYPTION_KEY is still accepted. |
PORT |
8000 |
HTTP listen port. |
DEV |
False | Truthy enables insecure development cookies, permits ephemeral CSRF/MFA keys, and skips the emailed 2FA code at login. Never enable in production. |
DEV_FORCE_2FA |
False | Only read when DEV is truthy. Truthy restores the real emailed 2FA code at login while keeping dev cookies and CSRF handling, so the flow can be tested locally against Mailpit. |
RUN_MIGRATIONS |
False | Truthy applies pending Goose migrations before listening. |
MIGRATIONS_DIR |
./migrations |
Migration directory; the backend image uses /migrations. |
PUBLIC_APP_URL |
Empty | Public website origin used to construct email, portal, invite, and update links. Set the HTTPS production origin without a trailing path. |
TRUST_PROXY_HEADERS |
False | Trust proxy IP/scheme headers and enable proxy-aware HTTPS enforcement. Use only behind a trusted overwriting proxy. |
SELF_SERVICE_RETURN_TOKEN |
False | Only the exact value true returns magic-link tokens in API responses. Development convenience; do not enable in production. |
RABBITMQ_URL |
Required when admin 2FA is enabled | AMQP URL. The backend publishes admin 2FA and transactional-email events to clave.events; set the same broker on the emailer. |
WORKER_TOKEN |
Required for delta worker | Shared bearer token protecting /api/v1/worker/*; configure the same high-entropy value on the backend and delta worker. |
UPDATE_ARTIFACT_STORAGE_PATH |
./data/update-artifacts |
Persistent local storage for uploaded release artifacts. |
UPDATE_CHECK_RETENTION_DAYS |
90 |
Deletes update-check telemetry older than this many days every 24 hours. Set 0 to disable cleanup. |
LOG_LEVEL |
info |
debug, verbose, or trace enables debug logging; other values use info logging. |
VERBOSE_LOGGING |
False | Truthy also enables debug logging. |
OTEL_ENABLED |
False | Truthy enables OpenTelemetry initialization and export. |
OTEL_SERVICE_NAME |
clave-api |
OpenTelemetry service name. |
OTEL_EXPORTER_OTLP_ENDPOINT |
Empty | OTLP/HTTP collector endpoint, for example http://collector:4318. |
SMTP_HOST |
Empty | Legacy backend SMTP setting. It is loaded but the current email path does not use it; configure the emailer instead. |
SMTP_PORT |
587 |
Legacy backend SMTP port; currently unused. |
SMTP_USER |
Empty | Legacy backend SMTP username; currently unused. |
SMTP_PASS |
Empty | Legacy backend SMTP password; currently unused. |
MAIL_FROM |
noreply@clave.app |
Legacy backend sender; currently unused. The emailer uses EMAIL_FROM. |
Version-adoption check-ins are retained for 90 days. The dashboard counts each activation once using its latest report from the last 30 days.
| Variable | Required/default | Purpose |
|---|---|---|
SMTP_URL |
Required | Nodemailer URL: smtp://user:pass@host:587 or smtps://... for implicit TLS. URL-encode credentials. |
EMAIL_FROM |
Clave <no-reply@clave.dev> |
RFC 5322 From value for transactional messages. |
RABBITMQ_URL |
amqp://localhost |
Broker URL. Must point to the broker used by the backend. |
| Variable | Required/default | Purpose |
|---|---|---|
VITE_API_BASE_URL |
Empty | API origin baked into the static bundle at build time. Leave empty for the recommended same-origin /api proxy. |
The Go delta worker consumes durable delta.generate events and produces
byte-exact BSDIFF patches between adjacent compatible release artifacts. Release
publication and full downloads continue to work when the worker is unavailable.
The production Compose service runs one job at a time with a 1536 MiB container
limit.
| Variable | Required/default | Purpose |
|---|---|---|
API_URL |
http://localhost:8000 |
Backend base URL. |
WORKER_TOKEN |
Required | Bearer token shared with the backend. |
RABBITMQ_URL |
amqp://guest:guest@localhost:5672/ |
Broker containing the durable clave.delta.generate queue. |
DELTA_MAX_ARTIFACT_BYTES |
67108864 |
Hard ceiling for both source and target artifacts. Oversized jobs are skipped before BSDIFF starts. |
DELTA_MEMORY_BUDGET_BYTES |
1342177280 |
Go soft memory limit and input-limit basis. Effective maximum is min(max artifact bytes, memory budget / 17). |
DELTA_HTTP_TIMEOUT_SECONDS |
600 |
Timeout for each backend request and artifact transfer. |
Classic BSDIFF generation can use roughly 17 times the source size for suffix sorting. File-based downloads avoid duplicate transport buffers but do not remove that algorithmic memory cost. Keep AMQP prefetch and worker concurrency at one; on a 4 GiB Grid-1 node, start with the documented 64 MiB artifact ceiling and 1536 MiB container limit. Larger artifacts fall back to their full download.
ZIP deltas are useful only when archives are reproducible. Normalize timestamps, permissions and extra fields, sort entries consistently, and keep the same DEFLATE settings between releases. For example, build from an already normalized tree using a stable file list and metadata-stripping mode:
find app -print | LC_ALL=C sort | zip -X -@ release.zip
strip-nondeterminism release.zip # when availablezip -X removes platform-specific extra attributes; it does not normalize file
timestamps by itself. Set stable timestamps in the build tree (commonly via
SOURCE_DATE_EPOCH) before creating the archive. Otherwise even a small content
change can move the BSDIFF ratio above 70 percent and Clave records the job as
skipped.
| Variable | Required/default | Purpose |
|---|---|---|
E2E_BASE_URL |
Test default | Backend base URL used by Go end-to-end tests. |
DATABASE_URL |
Test/CLI default | Also used by E2E tests, createadmin, Goose, and psql recipes. |
POSTGRES_PASSWORD |
Production Compose required | Initializes PostgreSQL and interpolates the backend DSN; not read by Clave itself. |
RABBITMQ_PASSWORD |
Production Compose required | Initializes RabbitMQ and interpolates service AMQP URLs; not read directly by Clave. |
CLAVE_ENV_FILE |
.env.production |
Optional Compose-only override for the file injected into backend and emailer containers. |
NODE_ENV=production is set internally by the emailer image and is not a Clave
configuration input. Standard variables belonging to PostgreSQL, RabbitMQ,
Docker, Go, Bun, pnpm, or Vite are outside this application-specific list.
just test
# or
cd backend && go test ./...Clave uses Semantic Versioning: major releases may change license/API flows, minor releases add backward-compatible features, and patch releases contain backward-compatible fixes.