XCAICX docs Product Contact

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.

PieceValue
RuntimePython ≥ 3.11, FastAPI, uvicorn
DatabaseSQLite on a persistent volume
SecretsXCAICX_SIGNING_KEY, XCAICX_ADMIN_KEY — never in the image
HealthGET /healthz

Environment#

VariablePurpose
XCAICX_DBDatabase path. Default xcaicx.db.
XCAICX_ADMIN_KEYAdmin credential. The service refuses to boot without it.
XCAICX_SIGNING_KEYSigning key inline
XCAICX_SIGNING_KEY_FILESigning key file. Default keys/root_ed25519.key.
XCAICX_PRICING_JSONCatalogue override file
XCAICX_PUBLIC_DOCS1 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:

bash
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=false

Do 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#

bash
curl -s https://licensing.xcaicx.com/healthz

Do 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.
  • keygen writes the private key mode 600 and refuses to overwrite without --force.
  • Keep server/keys/*.key out of version control. Verify before any push that touches that directory:
    bash
    git diff --cached --name-only | grep -q 'keys/.*\.key' && echo "STOP"
    grep -rl 'PRIVATE KEY' $(git diff --cached --name-only) 2>/dev/null
    Only the public key belongs in a repository.
  • 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:

  1. Generate the new keypair; keep the old one signing.
  2. Ship a config update setting license_pubkey to the new public key on a pilot fleet.
  3. Cut the server over to signing with the new key.
  4. 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 list shows 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:

bash
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#

bash
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 chargeback

What to watch#

SignalWhy it matters
/healthz public keyA mismatch breaks every camera silently
Batches near exhaustionA 402 at a customer's factory stops their line
weak_binding clustersAn integrator deriving fingerprints from MACs
Activation rate spikesEither a big customer shipping, or a cloning attempt
Backup job successSilent backup failure is the classic way to lose billing data
Rate-limit 429s on /v1/activateA 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:

bash
export XCAICX_PRICING_JSON=/etc/xcaicx/pricing.json
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#

Commercial software. Use requires a valid XCAICX licence token. Questions an integrator cannot answer from this page belong in an email to [email protected] — and, usually, in a fix to this page.

© 2026 AZMX AI · xcaicx.com