Skip to content

Operations

1. Deployment

PropertyValue
Imageregistry/licensing:<tag>
Container Port3000 (external 31120)
Base Path/v1/api/licensing
Snowflake worker11
Run modesRUN_MODE=startup (default), RUN_MODE=migrate
Migration modebun run migrate / migrate:dev — seeds permissions + free-trial Policy (all alwaysRun)
ProbesGET /healthz (live), GET /readyz (ready) — IGNIS defaults
Dependencies at bootPostgreSQL (licensing schema), Redis cache, Identity JWKS

Traefik labels

yaml
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.licensing.rule=PathPrefix(`/v1/api/licensing`)"
  - "traefik.http.services.licensing.loadbalancer.server.port=3000"

Build & run scripts

ScriptPurpose
bun run rebuildClean + compile (always use this; prestart/pretest/premigrate chain into it)
bun run server:devDev server (NODE_ENV=development, .env.development)
bun run migrate:devRun migrations / seeds
bun run testTests (requires .env.test)
bun run lint:fixESLint + Prettier

2. Observability

SignalSourceWhere to look
Logsstdout (structured key-value, key: %s)kubectl logs <pod> / Loki
HealthGET /healthz, GET /readyzGateway portal
Audit traillicensing.LicenseEvent tableDB query only — no REST endpoint registered (RestPaths.LICENSE_EVENTS exists but unwired)

Key log fields & signals

Field / messageSourceNotes
requestIdheader X-Request-IdPropagated cross-service
Ed25519 private key not configuredpublishCertificate() warnCerts are being skipped — consumers see null
Application secret not configuredpublishCertificate() warnSame as above
Failed to update lastValidatedAtvalidate() errorNon-fatal (fire-and-forget)
Failed to verify license certificateLicenseMiddleware (consumer side)Bad/expired/tampered cert → fail-open null

Audit event types (LicenseEventTypes)

created · activated · deactivated · suspended · reinstated · renewed · expired · revoked. (No validated, no heartbeat.)

3. Security

ConcernMitigation
AuthNJWT (ES256, JWKS from identity) or HTTP Basic
AuthZCasbin via PolicyDefinition; permissions seeded by migrations 0001/0003/0004. License read routes temporarily authorize.skip — see ADR-0003
Cert signing keyEd25519 private key in APP_ENV_LICENSING_ED25519_PRIVATE_KEY — K8s Secret, never in code
Cert payload secretAPP_ENV_APPLICATION_SECRET (AES-256-GCM) — must match across signer and all verifiers
Cert tamper protectionEd25519 signature; verifier rejects unknown alg and bad signatures
Cert expirycertExpiresAt in payload + Redis EX — both bounded by APP_ENV_LICENSING_CERT_TTL_SECONDS
SecretsK8s Secret, mounted as env
TLSTerminated at gateway
Network policyCilium — allow only gateway + Redis + Postgres + identity JWKS

Fail-open trust: LicenseMiddleware treats Redis miss / verify failure / expiry as null ("unknown"), not "unlicensed". Consumer gates must decide explicitly how to treat null.

4. Runbook

4.1 Alert classes

AlertTriggerCheckFixEscalate
licensingHighErrorRate5xx >5% over 5mlogs level=errorInspect DB / Redis connectivityon-call backend
licensingCertsSkippedrepeated key not configured warnconfirm Ed25519 + app-secret env setrestore secrets, re-publish (see 4.2)on-call backend
licensingExpiryDriftlicenses past expiresAt still ACTIVATEDexpected — expiry is lazytrigger validate, or run a manual sweepdoc — usually not an incident

4.2 Common operations

OperationHow
Tail logskubectl logs -n <ns> -f deploy/licensing
Run migration / seedRUN_MODE=migrate bun run migrate (in pod)
Force-refresh a cached certificateNo dedicated endpoint. Trigger any lifecycle action that re-publishes: suspendreinstate, or renew. CRUD PATCH /licenses/{id} and feature edits do not re-publish.
Inspect audit trailQuery licensing.LicenseEvent directly (no REST surface)
Free an activation seatDELETE /activations/{id} (no audit event) or ActivationService.deactivate() (writes event)

4.3 Ed25519 keypair rotation

Rotate the public key on consumers first, then the private key on licensing, then re-publish. A private-key roll without re-publish leaves old certs unverifiable (consumers fail-open to null).

4.4 Known caveats (not bugs)

BehaviorDetail
Lazy expiryNo reaper. A license flips to EXPIRED only on the next validate(). Licenses nobody validates stay ACTIVATED past expiry.
No heartbeat / floating seatsIActivationConfig = { limit } only; no lastHeartbeatAt, no /validation/heartbeat.
Feature edits not propagatedvalidate() reads DB fresh (immediate), but the cached cert only refreshes on a lifecycle action.
Direct activation bypassPOST /activations skips the locked limit check; prefer validate(fingerprint) to enforce the limit.
SVC-00110 collisionService code clashes with @nx/outreach — see ADR-0002.

4.5 Cross-service consumer requirements

Each verifier service must set, matching the signer:

VariableRequirement
APP_ENV_APPLICATION_SECRETSame value as licensing (AES-256-GCM secret)
APP_ENV_LICENSING_ED25519_PUBLIC_KEYMatching Ed25519 public key (PEM)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.