# Licentia Core OEM — Distribution Kit

Run **Licentia Core OEM** on your own infrastructure. Licentia is the
license **server** you operate to issue and validate licenses for **your
own** products. This kit is everything you need to bring the stack up with
Docker: a `docker-compose.yml` that pulls Celestryll's published images, an
`.env` you fill in, and the steps below.

> **Prefer to let an AI agent do it?** If you use Claude Code (or a
> similar coding agent), point it at **[`CLAUDE-SETUP.md`](CLAUDE-SETUP.md)** —
> a runbook that automates most of this setup for you.

## What Licentia Core OEM is

Licentia is a multi-tenant licensing and usage-metering service. You use it
to issue licenses to your customers, validate them (online or air-gapped),
meter usage, raise threshold alerts, and keep a tamper-evident audit trail.
It ships as a REST **API** plus a background **worker** (alerting sweeps and
webhook delivery), with an optional cloud **dashboard**.

### How Licentia's own licensing works — read this

Unlike Celestryll's other OEM products (FormaPrint, StableFlow,
AureaBilling), **Licentia does not phone home**. There is no remote license
authority to validate against and no TenantId / LicenseKey / ApiKey to
configure — **you are the authority**. Instead, your instance's posture is
decided by the **edition of the SQL Server it runs against**:

| SQL edition | Posture | Behavior |
|---|---|---|
| **SQL Express** | Demo / evaluation | The engine enforces demo caps (10 GB/db, connection and events-per-day ceilings, a 30-day window) that escalate to a **hard stop** (HTTP 402). Fully exercisable for evaluation. |
| **SQL Standard / Enterprise / Azure SQL** | Production | Full features, no demo caps. |

So a Demo evaluation and a Production deployment run the **same image** —
the difference is which database you point it at. There are no runtime
license credentials in `.env`.

## Prerequisites

- **Docker** and **Docker Compose** (Docker Desktop, or Docker Engine +
  the `docker compose` plugin).
- A **Celestryll account / purchase** so you can pull the published
  `licentia-oem-api` and `licentia-oem-worker` images — sign up at
  **https://celestryll.com/get-started**. (The images carry no runtime
  license check; your entitlement is your right to run them plus OEM
  support.)
- A **SQL Server or Azure SQL database** you control (the connection string
  is yours). For **evaluation**, SQL Express is fine (you get the demo
  caps). For **production**, use SQL Standard/Enterprise or Azure SQL. No
  database of your own? See the optional bundled `sql` service in
  `docker-compose.yml`.
- A SQL client to apply the schema once — `sqlcmd`, Azure Data Studio, or
  SSMS.
- `openssl` (or any strong RNG) to generate the internal secrets.

## Setup

### 1. Get the images

Make sure you can pull the published images (they are hosted on GitHub
Container Registry and published by Celestryll):

```bash
docker pull ghcr.io/mattshanaman/licentia-oem-api:latest
docker pull ghcr.io/mattshanaman/licentia-oem-worker:latest
```

### 2. Configure `.env`

Copy the template and fill it in:

```bash
cp .env.template .env
```

Edit `.env`:

- **From Celestryll:** *nothing* — Licentia has no runtime license
  credentials (see the licensing note above).
- **Your infrastructure:** `LICENTIA_CONNECTION` (your SQL connection
  string), `LICENTIA__SCHEMANAME` (leave `licentia` unless you share one
  database across products).
- **Secrets you generate:** step 3.

`.env` is git-ignored. Never commit it or paste it into shared output.

### 3. Generate the internal secrets

Three secrets are yours to generate — they are **not** provided by
Celestryll, and all three are **required in Production** (the app fails
fast at startup if any is missing or left at a dev default). Use strong,
unique random values (32+ bytes):

```bash
# JWT signing key (signs/validates Licentia's API bearer tokens)
openssl rand -base64 48

# Webhook signing secret (HMAC-signs outbound alert webhooks)
openssl rand -base64 48

# Audit HMAC key (keys the tamper-evident audit chain)
openssl rand -base64 48
```

Put the three values in `.env` as `JWT__SIGNINGKEY`,
`LICENTIA__WEBHOOKS__SIGNINGSECRET`, and `LICENTIA__AUDIT__HMACKEY`. Store a
copy in your secret manager — losing the audit key means older audit rows
can no longer be re-verified, and the **same** audit key must be used by the
API, worker, and (optional) dashboard.

**Optional — offline activation keypair.** Only if you will issue
offline/air-gapped activation files for your products, generate an RSA
keypair (the private key signs, the public key verifies):

```bash
mkdir -p secrets
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out secrets/offline-signing.key.pem
openssl rsa -in secrets/offline-signing.key.pem -pubout -out secrets/offline-verify.pub.pem
```

Then set in `.env` (the `./secrets` directory is mounted read-only into the
container at `/secrets`):

```
LICENTIA__OFFLINE__SIGNINGKEYPEM=file:/secrets/offline-signing.key.pem
LICENTIA__OFFLINE__VERIFYKEYPEM=file:/secrets/offline-verify.pub.pem
```

`secrets/` and `*.pem` are git-ignored. Leave both blank to run online-only.

### 4. Create the database

Create an empty database (default name `LicentiaCoreOEM`) on your SQL
Server / Azure SQL, connecting as an admin (`sa` or a DB owner):

```sql
IF DB_ID('LicentiaCoreOEM') IS NULL CREATE DATABASE LicentiaCoreOEM;
```

### 5. Apply the database schema

Licentia's tables, stored procedures, and demo seed data are created by an
ordered set of SQL scripts, **shipped in this kit's [`./sql/`](sql/)
directory**. This kit includes an **apply-schema helper** that runs every
script in numeric order with the required settings — use it rather than
hand-running each file:

```bash
# bash (Linux/macOS/WSL/Git Bash) — from the kit directory
./apply-schema.sh --database LicentiaCoreOEM --schema licentia \
  --server localhost,1433 --user sa --password '<sa-password>'
```

```powershell
# PowerShell (Windows)
./apply-schema.ps1 -Database LicentiaCoreOEM -Schema licentia `
  -Server localhost,1433 -User sa -Password '<sa-password>'
```

The helper connects as an **admin/migrator** (`sa` or a DB owner), creates the
database if absent, and applies every `./sql/*.sql` in numeric order, passing
your `SchemaName` (default `licentia`). It always sets `QUOTED_IDENTIFIER ON`
(`-I`) and aborts on error (`-b`). The scripts are idempotent (safe to re-run
on upgrade). You need a `sqlcmd` on your PATH (mssql-tools18) or pass
`--sqlcmd <path>`.

- **`sa` is only for this one-time schema apply.** Your app's **runtime**
  connection string (`LICENTIA_CONNECTION`) must use the least-privilege
  `licentia_app` login that `17_AppSecurity.sql` creates (dev password
  `Licentia!App#Dev1`; set your own with `ALTER LOGIN`) — Production refuses an
  `sa` runtime connection at boot.
- **On Azure SQL, skip `17_AppSecurity.sql`.** It runs `CREATE LOGIN` and
  `GRANT VIEW SERVER STATE` (server-level statements Azure SQL doesn't support
  this way). Apply the rest, then create a contained user for the app and grant
  it `SELECT/INSERT/UPDATE/DELETE/EXECUTE` on `SCHEMA::licentia` only. (The
  helper applies every script; on Azure SQL, move `17_AppSecurity.sql` out of
  `./sql/` first, or run the equivalent grants by hand.)
- The helper applies **every** script in `./sql/`, including
  `14_UsageEventOwnership.sql` (a hardening also folded into a fresh
  `03_Usage_Procs.sql`) and `22_UsageReportMarkers.sql` (the opt-in usage
  reporter's tables) — both are idempotent and harmless on a fresh database.

### 6. Start the stack

```bash
docker compose up -d
```

This pulls Celestryll's published API and worker images and starts them
against your database.

### 7. Verify health

```bash
# API — public health endpoint, includes a SQL readiness check.
# This is the API's ONLY health path (there is no /health/ready on the API).
curl -fsS http://localhost:8080/api/health
#   -> {"status":"Healthy","checks":{"sql":"Healthy"}, ...}
```

A `Healthy` status with `"sql":"Healthy"` confirms the API reached your
database. (Adjust the port if you changed `LICENTIA_API_PORT`.)

The **worker** does not publish a host port, so you don't curl it from the
host — Compose runs its `/health/ready` probe **inside** the worker container
(see the `healthcheck` block in `docker-compose.yml`). Check it with:

```bash
docker compose ps            # worker should show "healthy"
docker compose logs licentia-worker
```

### 8. Issue a first license (smoke test)

Every admin call is authenticated with a bearer token signed by **your**
`JWT__SIGNINGKEY`. Mint a short-lived **Admin** service token with that key
(HS256, `iss=LicentiaCoreOEM`, `aud=LicentiaCoreOEM`, `scope=Admin`) using
any JWT tool, then issue a license for the seeded demo OEM partner and
tenant (`07_SeedData.sql` creates one of each; on a fresh database they are
`PartnerId=1` and `TenantId=1`):

```bash
TOKEN=<Admin service token signed with your JWT signing key>

curl -fsS -X POST http://localhost:8080/api/licenses \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "PartnerId": 1, "TenantId": 1, "LicenseType": "Demo",
        "Plan": "Evaluation", "DurationDays": 30, "MaxTenants": 1 }'
# -> 201 Created  { "success": true, "data": { "licenseId": ..., "licenseKey": "...", ... } }
```

A `201` with a `licenseKey` means the API, database, audit chain, and auth
are all working end-to-end. (If the seeded ids differ on your database, list
them with an admin `GET /api/admin/tenants` or via the optional dashboard.)

## Configuration reference

| `.env` variable | Container key | Who supplies it | Purpose |
|---|---|---|---|
| *(none)* | — | Celestryll | No runtime license credentials — Licentia does not phone home |
| `LICENTIA_CONNECTION` | `Licentia__ConnectionString` | You | Your SQL connection string (edition decides demo vs production) |
| `LICENTIA__SCHEMANAME` | `Licentia__SchemaName` | You | SQL schema (default `licentia`) |
| `JWT__SIGNINGKEY` | `Jwt__SigningKey` | You (generate) | Signs/validates API bearer tokens |
| `LICENTIA__WEBHOOKS__SIGNINGSECRET` | `Licentia__Webhooks__SigningSecret` | You (generate) | HMAC-signs outbound alert webhooks |
| `LICENTIA__AUDIT__HMACKEY` | `Licentia__Audit__HmacKey` | You (generate) | Keys the tamper-evident audit chain (same on api/worker) |
| `LICENTIA__OFFLINE__SIGNINGKEYPEM` | `Licentia__Offline__SigningKeyPem` | You (generate, optional) | RSA private key that signs offline activation files |
| `LICENTIA__OFFLINE__VERIFYKEYPEM` | `Licentia__Offline__VerifyKeyPem` | You (generate, optional) | RSA public key that verifies offline activation files |
| `LICENTIA_VERSION` | image tag | Optional | Published image tag (default `latest`) |
| `LICENTIA_API_PORT` | host port | Optional | Host port for the API (default `8080`) |
| `LICENTIA_DASHBOARD_PORT` | host port | Optional | Host port for the optional dashboard (default `8081`) |

## Upgrading

Pin a version in `.env` and bump it when Celestryll publishes a release:

```bash
# in .env
LICENTIA_VERSION=1.4.0

docker compose pull
docker compose up -d
```

Re-apply the `sql/` scripts for the new version (they are idempotent) if the
release notes call for schema changes.

## Troubleshooting

- **API won't start, complains a value is a "development default" or "not
  set"** — in Production the app fails fast on missing/default secrets.
  Ensure `JWT__SIGNINGKEY`, `LICENTIA__WEBHOOKS__SIGNINGSECRET`, and
  `LICENTIA__AUDIT__HMACKEY` are all set to strong, non-default values (32+
  bytes). Check logs: `docker compose logs licentia-api`.
- **API won't start, complains the connection authenticates as `sa`** — in
  Production Licentia refuses `sa`. Use the least-privilege `licentia_app`
  login (from `17_AppSecurity.sql`) in `LICENTIA_CONNECTION`.
- **`/api/health` reports `sql` Unhealthy** — database connectivity. Verify
  `LICENTIA_CONNECTION` (host, port, credentials, `Encrypt` /
  `TrustServerCertificate`), that the database exists, and that the schema
  scripts were applied.
- **Tenants get HTTP 402 / `HARD_STOP_SQL_EXPRESS`** — you are running
  against SQL Express (the demo posture) and hit a demo cap. Move to SQL
  Standard/Enterprise or Azure SQL for production; a Production license on
  Express also raises a Critical edition-mismatch alert.
- **`401 Unauthorized` issuing a license** — your bearer token isn't valid
  for **your** `JWT__SIGNINGKEY`, or it lacks `scope=Admin` (or `Oem`).
  Re-mint the token with the current key and an `Admin` scope (see step 8).
- **Audit chain won't verify across services** — the API and worker must use
  the **same** `LICENTIA__AUDIT__HMACKEY`. Confirm both containers received
  the identical value.

## Support

- Get started / manage your account: **https://celestryll.com/get-started**
- Support: **support@celestryll.com**

When contacting support, include the API logs
(`docker compose logs licentia-api`). **Never** send your JWT signing key,
webhook signing secret, audit HMAC key, or offline private key.
