Operations & migration

How to know a SuperDRM install is healthy, what to back up, how to prove it still works, and how to move it to another host without downtime.

#Health

  • GET /health on the api: 200 when the vault and database answer, 503 otherwise. The body names the vault provider, the database, the FairPlay KSM state, the Widevine environment and — on-prem — the install licence.
  • GET /healthz on docs and live.
  • The live console streams every licence event and shows the whole system as a node canvas.
Shell
curl -s https://api.<domain>/health | jq '{ok, vault, db, fairplay_ksm, widevine, install}'

#Monitoring

  • /health every minute from each region you serve.
  • A canary licence every 5 minutes: issue a key, request a Clear Key licence for it, and (for Widevine) a service-certificate request. Alert on any non-200.
  • Licence failures by error code: a spike in hw_required or token_invalid is a client regression; a spike in upstream is Google or the KSM.
  • KEK health, and expiry of partner certificates (the FairPlay application certificate is annual).

#Reading the meter

  • Healthy: widevine kind=license ok=true security=L3|L1, fairplay … security=hw-main.
  • Files packaged before a vendor switch: ok=false error=no_key in a couple of milliseconds, then your previous vendor serves the licence. These rows are the fallback working, not an outage; they fade as old files are re-packaged or expire.
  • hw_required: a client policy regression (software CDM where hardware was required).
  • upstream: Google or the KSM; check /health and the provider's status page.
Shell
superdrm licences --limit 20
superdrm usage --tenant acme --from 2026-09-01
superdrm audit --limit 50

#Keys and backups

  • The KEK is the one thing that cannot be recreated. Under local it is a file (secrets/kek.hex in Compose); back it up offline now — losing it loses every sealed key. Under pkcs11 or aws-kms back it up with the token's own mechanism (Hardware key vault).
  • The database holds the sealed rows: pg_dump nightly, kept separately from the KEK.
  • Restore drill: new stack → superdrm import (or psql < backup) → superdrm verify-kek.

#Upgrades

Images are tagged by git SHA and by version. Pull the new tag, restart api first, then console, docs and live. Migrations are additive and idempotent and apply at api boot (or via the Helm hook). Roll back by pointing the tag back; schema changes never drop columns a previous version still reads.

#Re-runnable proofs

ProofCommandExpect
Unit + APInpm testall pass
Live Widevine key registrationSUPERDRM_TEST_LIVE_WIDEVINE=1 npm testthe LIVE: test prints a UAT key and PSSH
A browser plays SuperDRM-keyed content with a SuperDRM licencenode scripts/prove-widevine.mjsPROOF OK, a widevine … ok=true row
Bring-your-own-key licence with our own PSSHnpx tsx scripts/prove-byok.mtsBYOK PROOF OK
PKCS#11 vault against a real tokennpm run test:pkcs11PASS: pkcs11 vault proven against SoftHSM2
End to end from your packagerPackage a file, view it, then superdrm licences --limit 5an ok=true row for the new content id

#Server migration

SuperDRM keeps two things you must carry: the database and the KEK. Every content key, tenant token secret and partner credential in the database is sealed under the KEK; without it the export is ciphertext. Everything else (images, env) is reproducible.

#The tools

Shell
superdrm export superdrm-2026-09-11.json.gz            # from the env the CLI points at (DATABASE_URL or PGlite dir)
superdrm import superdrm-2026-09-11.json.gz            # into DATABASE_URL (runs migrations, upserts, idempotent)
superdrm verify-kek [content id]                       # opens one sealed key with the configured vault
scripts/migrate-server.sh export|import|verify …       # wraps the three with the KEK warning and optional --reseal-to

The bundle is gzipped JSON: every table (tenants, api_keys, tenant_drm, partner_creds, content_keys, licences, usage_daily, audit_log, plans, onprem_*, superdrm_migrations) with bytea as base64, plus the source KEK id. Import is an upsert on each table's primary key, so re-running it is safe, and identity sequences are advanced past the imported ids.

#Downtime-free cutover

  1. Stand up the target (Compose or Helm) with the same KEK: copy kek.hex, or point at the same HSM/KMS key. Start it on the target's private address; do not expose it yet.
  2. Freeze packaging on the source tenants (pause the packager worker). Licences keep flowing from the source.
  3. Export from the source and import into the target. Run verify on the target: row counts match and one sealed key opens.
  4. Switch the edge: point the tunnel route, DNS or load balancer for api.<domain> at the target. Players' next licence request lands on the target; nothing in the client changes.
  5. Unfreeze packaging with the packager's SUPERDRM_URL pointing at the target.
  6. Catch up: export/import once more to carry the licence rows issued on the source between steps 3 and 4 (upserts, so it is safe).
  7. Retire the source after a day of clean metering on the target.

If the target uses a different vault (say, from a software KEK to AWS KMS): import with the source KEK available as SUPERDRM_OLD_* and run --reseal-to aws-kms; the re-seal opens each blob with the old vault and seals it with the new one, in batches, verifying every row and updating kek_id.

#What a rehearsal looks like

A migration from a single-node PGlite install into a Compose stack on Postgres 16, performed on real data: a 6.6 KB bundle (plans, tenants, API keys, content keys, licences, daily usage, audit rows, migrations) imported with kek_match: true and identical counts, idempotent on re-run; verify-kek opened an existing content key on the target; the target then served a Clear Key licence for an existing content id and issued a new Widevine key against Google's UAT service; api, console, docs and live all healthy. Re-run it any time on a scratch host.

Updated September 2026