Installation & Setup
Get started with the License Server API in minutes. This guide covers deployment options, configuration, and initial setup.
Don't want to run your own server?
This page covers self-hosting. If you'd rather sign up for a managed account instead, see Hosted (SaaS) Tier — same API, no deployment required.
Quick Start
Prefer a ready-made bundle?
Download the self-hosted quickstart package — a docker-compose.yml, .env.example, and a short README, already wired up for the steps below. Unzip it, follow its own README, and skip straight to License Activation once it's running.
Using Docker (Recommended)
Pull the published image — no source access needed. The four required secrets (below) don't need to be generated by hand: set AUTO_GENERATE_SECRETS=true and the container generates and persists them itself on first boot.
mkdir -p data
docker pull ghcr.io/casazium/license:latest
docker run -d \
--name casazium-license \
-p 127.0.0.1:3001:3001 \
-v "$(pwd)/data:/app/data" \
-e DB_FILE=./data/license.db \
-e AUTO_GENERATE_SECRETS=true \
--restart unless-stopped \
ghcr.io/casazium/license:latestThe server will be available at http://localhost:3001.
Using Docker Compose
The same image, via Compose - convenient once you're managing more than one service, or want restart: unless-stopped and the volume mount declared in a file rather than a long docker run line:
# docker-compose.yml
version: '3.9'
services:
license:
image: ghcr.io/casazium/license:latest
ports:
- '127.0.0.1:3001:3001'
env_file:
- .env
volumes:
- ./data:/app/data
environment:
- DB_FILE=./data/license.db
- AUTO_GENERATE_SECRETS=true
restart: unless-stoppeddocker compose up -dEnvironment Configuration
Required Environment Variables
The server refuses to start unless these four are set (src/lib/config.js validates them at boot):
ADMIN_API_KEY=your-secure-admin-api-key-here
ENCRYPTION_KEY=your-32-byte-hex-encryption-key-here
LICENSE_SIGNING_SECRET=your-license-signing-secret-here
LICENSE_RSA_PRIVATE_KEY=your-pem-encoded-rsa-private-key-hereDon't want to generate these yourself?
Set AUTO_GENERATE_SECRETS=true (shown above) instead of setting these four directly, and leave them blank. The container generates all four on first boot and saves them to <your data directory>/.secrets.json — the same directory your SQLite database already lives in — so every later restart reuses the same values instead of minting new ones. Back that file up along with the rest of your data directory: losing it invalidates every license file you've exported and your admin API key. If you set any of the four explicitly, that value always wins and is never overwritten, even with AUTO_GENERATE_SECRETS=true set — useful if you're restoring a previous deployment's values rather than starting fresh. The "Generating Secure Keys" section below still applies if you'd rather generate and manage these yourself.
License Activation
The public ghcr.io/casazium/license image is gated by an activation license — without one configured, the server refuses to start immediately, before it ever opens its database or starts listening. This is separate from the four required environment variables above, checked at a different stage of boot, so it's easy to miss if you're only working from the four-variable list.
Set one of:
# A license file (the normal case for Docker/Compose) - mount it into
# the container and point this at the mounted path:
TIER_A_LICENSE_FILE=/app/license.json
# ...or the same file's raw JSON contents inline, if mounting an extra
# file isn't convenient for your deployment style:
TIER_A_LICENSE='{"payload": {...}, "signature": "..."}'You'll receive your license.json file after purchase. If you don't have one yet and want to evaluate first:
# Time-limited evaluation window, logged loudly on every boot. Not for
# production use - and the window is measured from when this image was
# built, not from when you first set this flag.
CASAZIUM_UNLICENSED_EVAL=1The evaluation window resets on every new latest pull
The 30-day window is measured from the image's own build time, baked in when it was published - not from the first time you ran it. Every new ghcr.io/casazium/license:latest build (even one shipping an unrelated fix) gets a fresh build time, so docker pull followed by a restart gives you a new 30-day window from that build, regardless of how long you'd already been running the previous image. Don't rely on this for anything beyond genuinely trying the product out - it isn't a way to extend an evaluation indefinitely on purpose, and a real purchased license removes the whole question.
If Casazium stops offering the License Server
A purchased license doesn't leave you dependent on Casazium staying in business. The License Server end-user license agreement commits to:
- 90 days' notice. At least 90 days' written notice by email before Casazium stops offering or supporting the License Server, or stops operating.
- A final build that needs nothing from Casazium. Before that date, everyone holding a valid, unexpired license when the notice is sent gets a final build with the activation-license check and all communication with Casazium's servers removed, which they can keep running indefinitely under the same license terms.
- No degraded installs. If Casazium's license-validation servers go away, that is never grounds for your installation to stop working or lose functionality.
An evaluation-mode install (CASAZIUM_UNLICENSED_EVAL) has no license, so it isn't covered.
Verifying activation
Check the container logs after starting:
docker logs <container-name> | grep tier-a-licenseA successful activation logs tier-a-license: valid activation license found with the name it was issued to and its expiry date. Evaluation mode logs tier-a-license: no valid activation license - proceeding under a time-limited evaluation allowance, not for production use (expires <timestamp>) - that timestamp is when this image's evaluation window itself runs out, per the note above. A refused boot logs one of two exact messages depending on why: tier-a-license: refusing to start - no valid activation license found. Contact your vendor to obtain one., or, if a license was found but its evaluation window ran out, tier-a-license: refusing to start - the evaluation allowance window for this build has expired. Either way, the container exits before ever accepting a connection.
Optional Environment Variables
These all have working defaults and don't need to be set:
PORT=3000 # app default: 3000 (the Docker image sets ENV PORT=3001 - see Quick Start above)
NODE_ENV=production # default: development
DB_FILE=./data/license.db # default: ./dev.db
ADMIN_RATE_LIMIT_MAX=300 # default: 300 — see Rate Limits
MULTI_TENANT=false # default: false — see Hosted (SaaS) Tier
REPORT_EXTRACT_KEY= # default: unset — see below
NOTIFICATIONS_EXTRACT_KEY= # default: unset — see belowThe app itself defaults PORT to 3000 — 3001 is only what you get via the published Docker image (which bakes ENV PORT=3001) or the Quick Start's docker run -p 127.0.0.1:3001:3001 command above. If you're running the SEA executable directly (below) with no PORT set, it listens on 3000, not 3001.
MULTI_TENANT=true switches this deployment into multi-tenant mode (many accounts sharing one running instance, each isolated to its own data) — see Hosted (SaaS) Tier. Leave it unset/false for an ordinary single-tenant self-hosted deployment, which is what the rest of this page assumes.
REPORT_EXTRACT_KEY gates GET /admin/report-extract, a separate credential from ADMIN_API_KEY by design — leaving it unset simply means that one route rejects every request; nothing else about the deployment is affected. Comma-separated to support rotation (add the new key, then remove the old one, with no window where neither works).
NOTIFICATIONS_EXTRACT_KEY gates GET /admin/expiring-licenses the same way, as its own separate credential — same comma-separated rotation support, same fail-closed-when-unset behavior, just a different route.
Security Warning
Never commit real secrets to version control! Use secure methods to manage production secrets:
- Environment variables
- Docker secrets
- Cloud secret managers (AWS Secrets Manager, Azure Key Vault, etc.)
- HashiCorp Vault
Generating Secure Keys
Only needed if you're setting the four required secrets explicitly instead of using AUTO_GENERATE_SECRETS=true (above) — for example, restoring a previous deployment's values, or a Kubernetes deployment running multiple replicas against shared secrets (auto-generation is a single-instance convenience; a multi-replica deployment needs every replica to agree on the same values, which is exactly what Kubernetes Secrets, used in the Kubernetes section below, are for). Generate cryptographically secure keys for production:
# Generate admin API key (32+ characters recommended)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Generate encryption key (must be exactly 32 bytes / 64 hex chars)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Generate signing secret (32+ characters recommended)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Generate RSA keypair for offline license verification (private key only —
# the public key is derived at runtime and served from GET /v1/public-key)
node -e "console.log(require('crypto').generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } }).privateKey)"This same key also signs update manifests if you use Software Distribution — see that page's note on why it must persist across redeploys.
Deployment Options
The Docker and Docker Compose paths are covered in Quick Start above - this section covers the remaining options.
Single Executable (SEA) Binary
No Docker required - a self-contained executable for Linux and macOS (Apple Silicon) with Node itself, this application, and its dependencies already bundled in. Gated by the same activation license as the Docker image (see License Activation above) and reads the same required/optional environment variables - just export them in your shell or process manager instead of docker run -e/an env file.
# Linux (x64)
curl -L -o casazium-license https://github.com/casazium/license-releases/releases/latest/download/casazium-license-linux-x64
chmod +x casazium-license
# macOS (Apple Silicon)
curl -L -o casazium-license https://github.com/casazium/license-releases/releases/latest/download/casazium-license-darwin-arm64
chmod +x casazium-licenseexport ADMIN_API_KEY=... ENCRYPTION_KEY=... LICENSE_SIGNING_SECRET=... LICENSE_RSA_PRIVATE_KEY=...
export TIER_A_LICENSE_FILE=/path/to/license.json
export DB_FILE=./data/license.db
./casazium-licenseThese URLs always resolve to the newest published build - there's no version number to update here as new releases ship. Only linux-x64 and darwin-arm64 builds are published today; other platforms aren't available yet.
Kubernetes
Deploy to Kubernetes with secrets management:
# license-server-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: license-server-secrets
type: Opaque
stringData:
admin-api-key: "your-admin-api-key"
encryption-key: "your-encryption-key"
signing-secret: "your-signing-secret"
rsa-private-key: "your-pem-encoded-rsa-private-key"
---
# license-server-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: license-server
labels:
app: license-server
spec:
replicas: 2
selector:
matchLabels:
app: license-server
template:
metadata:
labels:
app: license-server
spec:
containers:
- name: license-server
image: ghcr.io/casazium/license:latest
ports:
- containerPort: 3001
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3001"
- name: ADMIN_API_KEY
valueFrom:
secretKeyRef:
name: license-server-secrets
key: admin-api-key
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: license-server-secrets
key: encryption-key
- name: LICENSE_SIGNING_SECRET
valueFrom:
secretKeyRef:
name: license-server-secrets
key: signing-secret
- name: LICENSE_RSA_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: license-server-secrets
key: rsa-private-key
- name: DB_FILE
value: "/app/data/license.db"
volumeMounts:
- name: data-volume
mountPath: /app/data
livenessProbe:
httpGet:
path: /
port: 3001
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 3001
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: license-server-pvc
---
# license-server-service.yaml
apiVersion: v1
kind: Service
metadata:
name: license-server-service
spec:
selector:
app: license-server
ports:
- protocol: TCP
port: 80
targetPort: 3001
type: ClusterIPDeploy to Kubernetes:
kubectl apply -f license-server-secret.yaml
kubectl apply -f license-server-deployment.yamlVerification
Health Check
Verify the server is running (the health check is at the server root, not under /v1):
curl http://localhost:3001/Expected response:
{
"message": "License API is running",
"version": "1.0.0",
"buildFingerprint": "a1b2c3d4e5f6"
}Admin Authentication Test
Test admin endpoints:
curl -H "Authorization: Bearer your-admin-api-key" \
http://localhost:3001/v1/admin/statsExpected response:
{
"totalLicenses": 0,
"activeLicenses": 0,
"revokedLicenses": 0,
"totalActivations": 0,
"recentActivations": []
}Configuration Options
Database Configuration
The License Server uses SQLite by default. Configure the database location:
# Relative path
DB_FILE=./data/license.db
# Absolute path
DB_FILE=/var/lib/license-server/license.db
# In-memory (development only)
DB_FILE=:memory:Database Backups
For production deployments:
- Regular backups of the SQLite database file
- Consider database replication for high availability
- Monitor database size and performance
Rate Limits
Most rate-limit tiers are fixed values set in the server implementation. The one exception is the admin-info tier (list-licenses, admin/stats, recent-activations, and related read endpoints), whose limit is configurable via ADMIN_RATE_LIMIT_MAX (default 300 requests per 15 minutes). See Rate Limits for the full breakdown.
Logging Configuration
Logging is on/off, not independently configurable — there's no LOG_LEVEL, LOG_FORMAT, or LOG_FILE environment variable read anywhere in the server. It's driven by NODE_ENV: any value other than test enables Fastify's built-in Pino logger, which writes structured JSON to stdout at Pino's own default info level; NODE_ENV=test disables logging entirely (used by the test suite to keep output quiet). If you need a different level, format, or a log file, capture stdout with your process manager or container runtime (e.g. Docker's own logging driver — see the logging: block in docker-compose-coolify.yml) rather than looking for an in-app setting.
Next Steps
Now that your License Server is running:
- Set up authentication - Configure admin API keys
- Understand rate limits - Learn about request quotas
- Issue your first license - Create a test license
- Integrate with your app - Add license verification
- Gate your own updates - License-gated releases and signed auto-update checks for your end users
Troubleshooting
Common Issues
Port already in use:
# Check what's using port 3001
lsof -i :3001
# Use a different port - change the published port on the left side of
# the -p/ports mapping (the container still listens on 3001 internally)
docker run -p 127.0.0.1:3002:3001 ... ghcr.io/casazium/license:latestDatabase permission errors:
# Ensure the data directory exists and is writable
mkdir -p ./data
chmod 755 ./dataInvalid encryption key:
# Encryption key must be exactly 64 hex characters (32 bytes)
node -e "
const key = 'your-key-here';
console.log('Key length:', key.length, '(should be 64)');
console.log('Valid hex:', /^[0-9a-f]{64}$/i.test(key));
"Support
If you encounter issues:
- Check the troubleshooting guide
- Review server logs for error messages
- Email hello@casazium.com
