Skip to content

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.

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.

bash
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:latest

The 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:

yaml
# 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-stopped
bash
docker compose up -d

Environment Configuration

Required Environment Variables

The server refuses to start unless these four are set (src/lib/config.js validates them at boot):

bash
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-here

Don'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:

bash
# 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:

bash
# 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=1

The 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:

bash
docker logs <container-name> | grep tier-a-license

A 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:

bash
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 below

The 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:

bash
# 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.

bash
# 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-license
bash
export 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-license

These 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:

yaml
# 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: ClusterIP

Deploy to Kubernetes:

bash
kubectl apply -f license-server-secret.yaml
kubectl apply -f license-server-deployment.yaml

Verification

Health Check

Verify the server is running (the health check is at the server root, not under /v1):

bash
curl http://localhost:3001/

Expected response:

json
{
  "message": "License API is running",
  "version": "1.0.0",
  "buildFingerprint": "a1b2c3d4e5f6"
}

Admin Authentication Test

Test admin endpoints:

bash
curl -H "Authorization: Bearer your-admin-api-key" \
     http://localhost:3001/v1/admin/stats

Expected response:

json
{
  "totalLicenses": 0,
  "activeLicenses": 0,
  "revokedLicenses": 0,
  "totalActivations": 0,
  "recentActivations": []
}

Configuration Options

Database Configuration

The License Server uses SQLite by default. Configure the database location:

bash
# 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:

  1. Set up authentication - Configure admin API keys
  2. Understand rate limits - Learn about request quotas
  3. Issue your first license - Create a test license
  4. Integrate with your app - Add license verification
  5. Gate your own updates - License-gated releases and signed auto-update checks for your end users

Troubleshooting

Common Issues

Port already in use:

bash
# 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:latest

Database permission errors:

bash
# Ensure the data directory exists and is writable
mkdir -p ./data
chmod 755 ./data

Invalid encryption key:

bash
# 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: