Operations
1. Deployment
| Property | Value |
|---|---|
| Image | registry/licensing:<tag> |
| Container Port | 3000 (external 31120) |
| Base Path | /v1/api/licensing |
| Snowflake worker | 11 |
| Run modes | RUN_MODE=startup (default), RUN_MODE=migrate |
| Migration mode | bun run migrate / migrate:dev — seeds permissions + free-trial Policy (all alwaysRun) |
| Probes | GET /healthz (live), GET /readyz (ready) — IGNIS defaults |
| Dependencies at boot | PostgreSQL (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
| Script | Purpose |
|---|---|
bun run rebuild | Clean + compile (always use this; prestart/pretest/premigrate chain into it) |
bun run server:dev | Dev server (NODE_ENV=development, .env.development) |
bun run migrate:dev | Run migrations / seeds |
bun run test | Tests (requires .env.test) |
bun run lint:fix | ESLint + Prettier |
2. Observability
| Signal | Source | Where to look |
|---|---|---|
| Logs | stdout (structured key-value, key: %s) | kubectl logs <pod> / Loki |
| Health | GET /healthz, GET /readyz | Gateway portal |
| Audit trail | licensing.LicenseEvent table | DB query only — no REST endpoint registered (RestPaths.LICENSE_EVENTS exists but unwired) |
Key log fields & signals
| Field / message | Source | Notes |
|---|---|---|
requestId | header X-Request-Id | Propagated cross-service |
Ed25519 private key not configured | publishCertificate() warn | Certs are being skipped — consumers see null |
Application secret not configured | publishCertificate() warn | Same as above |
Failed to update lastValidatedAt | validate() error | Non-fatal (fire-and-forget) |
Failed to verify license certificate | LicenseMiddleware (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
| Concern | Mitigation |
|---|---|
| AuthN | JWT (ES256, JWKS from identity) or HTTP Basic |
| AuthZ | Casbin via PolicyDefinition; permissions seeded by migrations 0001/0003/0004. License read routes temporarily authorize.skip — see ADR-0003 |
| Cert signing key | Ed25519 private key in APP_ENV_LICENSING_ED25519_PRIVATE_KEY — K8s Secret, never in code |
| Cert payload secret | APP_ENV_APPLICATION_SECRET (AES-256-GCM) — must match across signer and all verifiers |
| Cert tamper protection | Ed25519 signature; verifier rejects unknown alg and bad signatures |
| Cert expiry | certExpiresAt in payload + Redis EX — both bounded by APP_ENV_LICENSING_CERT_TTL_SECONDS |
| Secrets | K8s Secret, mounted as env |
| TLS | Terminated at gateway |
| Network policy | Cilium — allow only gateway + Redis + Postgres + identity JWKS |
Fail-open trust:
LicenseMiddlewaretreats Redis miss / verify failure / expiry asnull("unknown"), not "unlicensed". Consumer gates must decide explicitly how to treatnull.
4. Runbook
4.1 Alert classes
| Alert | Trigger | Check | Fix | Escalate |
|---|---|---|---|---|
licensingHighErrorRate | 5xx >5% over 5m | logs level=error | Inspect DB / Redis connectivity | on-call backend |
licensingCertsSkipped | repeated key not configured warn | confirm Ed25519 + app-secret env set | restore secrets, re-publish (see 4.2) | on-call backend |
licensingExpiryDrift | licenses past expiresAt still ACTIVATED | expected — expiry is lazy | trigger validate, or run a manual sweep | doc — usually not an incident |
4.2 Common operations
| Operation | How |
|---|---|
| Tail logs | kubectl logs -n <ns> -f deploy/licensing |
| Run migration / seed | RUN_MODE=migrate bun run migrate (in pod) |
| Force-refresh a cached certificate | No dedicated endpoint. Trigger any lifecycle action that re-publishes: suspend → reinstate, or renew. CRUD PATCH /licenses/{id} and feature edits do not re-publish. |
| Inspect audit trail | Query licensing.LicenseEvent directly (no REST surface) |
| Free an activation seat | DELETE /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)
| Behavior | Detail |
|---|---|
| Lazy expiry | No reaper. A license flips to EXPIRED only on the next validate(). Licenses nobody validates stay ACTIVATED past expiry. |
| No heartbeat / floating seats | IActivationConfig = { limit } only; no lastHeartbeatAt, no /validation/heartbeat. |
| Feature edits not propagated | validate() reads DB fresh (immediate), but the cached cert only refreshes on a lifecycle action. |
| Direct activation bypass | POST /activations skips the locked limit check; prefer validate(fingerprint) to enforce the limit. |
SVC-00110 collision | Service code clashes with @nx/outreach — see ADR-0002. |
4.5 Cross-service consumer requirements
Each verifier service must set, matching the signer:
| Variable | Requirement |
|---|---|
APP_ENV_APPLICATION_SECRET | Same value as licensing (AES-256-GCM secret) |
APP_ENV_LICENSING_ED25519_PUBLIC_KEY | Matching Ed25519 public key (PEM) |
5. Related Pages
- Configuration
- Certificate System
/runbook/— central runbook for cross-service incidents- Decisions