Running the authority
Deploying the licensing server, protecting the signing key, and making sure the billing records survive.
What the service is#
A FastAPI application over SQLite. Small on purpose: it issues credentials and counts seats, and both of those are jobs where boring and durable beats clever.
| Piece | Value |
|---|---|
| Runtime | Python ≥ 3.11, FastAPI, uvicorn |
| Database | SQLite on a persistent volume |
| Secrets | XCAICX_SIGNING_KEY, XCAICX_ADMIN_KEY — never in the image |
| Health | GET /healthz |
Environment#
| Variable | Purpose |
|---|---|
XCAICX_DB | Database path. Default xcaicx.db. |
XCAICX_ADMIN_KEY | Admin credential. The service refuses to boot without it. |
XCAICX_SIGNING_KEY | Signing key inline |
XCAICX_SIGNING_KEY_FILE | Signing key file. Default keys/root_ed25519.key. |
XCAICX_PRICING_JSON | Catalogue override file |
XCAICX_PUBLIC_DOCS | 1 re-enables /docs, /redoc, /openapi.json |
Note
Refusing to boot without an admin key is deliberate. The alternative — defaulting to an open admin surface that can mint unlimited licences — is a failure mode that would be discovered by someone else.
Deploying#
The reference deployment is Fly.io with an encrypted volume:
fly volumes create xcaicx_data --size 1 --region sin
fly secrets set XCAICX_SIGNING_KEY="$(cat keys/root_ed25519.key)"
fly secrets set XCAICX_ADMIN_KEY="$(python3 -c 'import secrets;print(secrets.token_urlsafe(40))')"
fly deploy --ha=falseDo not
--ha=false is not optional. A Fly volume cannot be shared, and two machines writing one SQLite file is corruption — of the records that decide what your customers owe you. Run exactly one machine.
Any host works: the requirements are one process, one persistent writable path, and TLS in front.
Custom domain#
Point a CNAME at the app and let the platform issue the certificate. If the DNS provider also proxies, put the record in DNS-only mode until the certificate issues — a proxy answering the validation request first makes issuance fail in a way that is tedious to diagnose. Turn proxying on afterwards for rate limiting.
After every deployment#
curl -s https://licensing.xcaicx.com/healthzDo not
Compare public_key against the key compiled into your shipped SDK. If they differ, every licence the server mints is rejected by every camera in the field, and the camera-side symptom is a bare BadSignature that points nowhere near the cause. This is the single highest-value post-deploy check.
Protecting the signing key#
It is the root of trust for every licence you will ever issue.
- Production: KMS or HSM. Never on the API server's disk if you can avoid it.
keygenwrites the private key mode 600 and refuses to overwrite without--force.- Keep
server/keys/*.keyout of version control. Verify before any push that touches that directory:bashOnly the public key belongs in a repository.git diff --cached --name-only | grep -q 'keys/.*\.key' && echo "STOP" grep -rl 'PRIVATE KEY' $(git diff --cached --name-only) 2>/dev/null - Back it up offline, before issuing the first licence against it. There is no recovery path: losing it means every future licence must be signed by a new key, which means new firmware.
Rotation#
The engine's license_pubkey config field overrides the compiled-in key at runtime. That makes rotation stageable:
- Generate the new keypair; keep the old one signing.
- Ship a config update setting
license_pubkeyto the new public key on a pilot fleet. - Cut the server over to signing with the new key.
- Bake the new key into the next firmware release and drop the config override.
Without that override, rotation would mean firmware and server changing in the same instant across an entire fleet, which is not a thing that happens cleanly.
The admin key#
It mints licences and reads every customer's usage. Treat it like a production database password.
- Store it in a password manager or platform secret store, not in a file in
/tmp— temp storage is wiped on reboot, and a key that exists only there is a key you are about to lose. - The platform will not give it back to you:
fly secrets listshows a digest, not the value. Your copy is the only readable one. - Rotating means updating the platform secret and your stored copy together. Verify:bash
curl -s -o /dev/null -w '%{http_code}\n' \ -H "X-Admin-Key: $XCAICX_ADMIN_KEY" \ https://licensing.xcaicx.com/v1/admin/fleet # expect 200
Backups#
The database holds activation records and live licence tokens. It is a billing record, and it is also, for anyone who obtains it, a list of your customers.
Do not
Never back up a live SQLite database with a file copy. A WAL-mode database copied byte-for-byte loses recent commits — which means losing activations, which means losing revenue. Use SQLite's backup API (sqlite3 .backup, or VACUUM INTO) so the dump is transactionally consistent.
A backup you have not restored is a hypothesis. Restore each dump locally and run an integrity check before keeping it:
sqlite3 restored.db 'PRAGMA integrity_check;'
sqlite3 restored.db '.tables'Sensible posture is several independent copies with different failure modes — platform snapshots, an off-platform scheduled dump, and long-term object storage — because they fail for different reasons. Retention should outlive your longest billing dispute; a 90-day artifact retention is shorter than most.
Do not commit dumps into git history. They contain customer records and live licence tokens.
Day-to-day operation#
xcaicxctl customer "CamCo" --kind oem
xcaicxctl batch --customer CUST-… --sku oem-industrial --units 5000 -o batch.token
xcaicxctl batch-status B-2026-…
xcaicxctl fleet
xcaicxctl invoice CUST-… --days 30
xcaicxctl revoke LIC-… --reason chargebackWhat to watch#
| Signal | Why it matters |
|---|---|
/healthz public key | A mismatch breaks every camera silently |
| Batches near exhaustion | A 402 at a customer's factory stops their line |
weak_binding clusters | An integrator deriving fingerprints from MACs |
| Activation rate spikes | Either a big customer shipping, or a cloning attempt |
| Backup job success | Silent backup failure is the classic way to lose billing data |
Rate-limit 429s on /v1/activate | A provisioning line outrunning the limit |
Batch exhaustion is worth alerting on before it happens. A camera that cannot activate at the factory is a production-line stoppage at your customer, and they will not experience it as a commercial issue.
Pricing overrides#
Prices are data, not logic:
export XCAICX_PRICING_JSON=/etc/xcaicx/pricing.json{"edge-complete": {"tiers": [[1, 590], [100, 470]]}}A partial patch: it changes the named fields of the named SKUs and leaves the rest of the catalogue alone. Restart to pick it up.
Next#
- Security & threat model — what the design does and does not defend
- Licensing REST API — every endpoint
- xcaicxctl CLI — the operator commands