Platform architecture

AVERASE is one web application, two bootable kiosks, and two small edge Workers, tied together by a single entitlement object — the license token. This page explains what each piece does, where trust lives, and the end-to-end lifecycle from download to certificate.

The pieces

The web application (Rails) is the single source of truth. It owns accounts, users, subscriptions, license tokens, assets, wipe results, and certificates. Every entitlement decision — may this stick unlock, may it wipe, has it exceeded quota — is made here and nowhere else. It also serves the operator-facing UI (dashboard, inventory, wipes, downloads) and the outbound surfaces: the public API and webhooks.

The download Worker serves the tokenized ISO. When an admin clicks Download, Rails mints a license token, signs a short-lived URL, and redirects the browser to this Worker. The Worker verifies the signature and streams the ISO from object storage, splicing the customer's pre-signed token into a fixed 4 KiB /averase.key slot mid-stream. It mints nothing and touches no database — the same splice serves both the engine and hotswap images.

The kiosks — the erasure engine and the hotswap station — are bootable Alpine live systems that run entirely from RAM. They read their token from /averase.key, phone home through the engine Worker, and perform the wipes. They render no certificates and hold no signing keys.

The engine Worker (averase) is a stateless gateway between the fleet and Rails. It verifies each device's Ed25519 token signature at the edge, then forwards the request to Rails over an HMAC-signed internal API. It owns no state — no database, no object storage, no key-value store.

Certificates are generated only in Rails: two signed PDFs per successful wipe — a Certificate of Sanitization and an Erasure Report — uploaded to private object storage (R2) and served through short-lived presigned links. Third-party deliveries — webhooks and ERP integrations — are sent from Rails after persistence, never from a Worker.

Why the Worker owns no state

Keeping the engine Worker stateless is deliberate:

  • One source of truth. Every allow/deny, every quota increment, and every persisted record lives in one database with one transaction model. There is no edge cache of entitlement to drift stale, so a revocation takes effect on the very next request.
  • Fail-closed by construction. If the Worker can't reach Rails, it answers 503 {"allow": false, "reason": "validation_unavailable"} — and a kiosk that can't get an affirmative answer doesn't unlock and doesn't wipe. An unreachable backend can never be mistaken for permission.
  • A small attack surface. The Worker holds only a public key (to verify device tokens) and an HMAC secret (to sign its forwards). Compromising it yields no customer data, no minting ability, and no certificate signing key.

The same fail-closed posture runs through the kiosk itself: no offline unlock exists, an unprovisioned stick is permanently locked, and every wipe is gated by a live check-in.

Trust boundaries

Four cryptographic contracts hold the system together. Each pairs a secret (or private key) on one side with its verifier on the other:

BoundaryMechanismHeld byVerified byProtects
Device tokenEd25519 (one fleet-wide keypair); compact JWS with claims sub (account), jti (token UUID), iat, expRails holds the seed and mintsEngine Worker holds the public keyOnly Rails can create a token a kiosk will present and the Worker will accept
Download linkHMAC-SHA256 over averase-iso-download-v3:{dl}:{acct}:{u}:{exp}:{iso}:{sha256(tok)}Rails signsDownload Worker verifiesLinks expire in 5 minutes and can't be replayed with a swapped token or a swapped image — the signature covers both the token hash and the ISO product
Internal APIHMAC-SHA256 over {timestamp}.{body}, sent as X-Averase-Timestamp + X-Averase-Signature (skew ±300s)Engine Worker signsRails verifiesOnly the Worker can call /internal/*; a forged or replayed request gets 401 {"error": "unauthorized"}
/averase.key slotFixed 4096-byte region: magic CWKEYSLOT-v1\n + CW_DEVICE_TOKEN=<jws>, newline-paddedWritten by the download Worker spliceRead at boot by the kioskOne byte-exact contract shared by the ISO build, the splice, and the boot reader — an unpatched (placeholder) slot leaves the stick unprovisioned and inert

A fifth key exists outside the device path: the certificate signing key, a separate Ed25519 pair used only by Rails to sign certificates (its public key is published for verification). It never ships in any image.

The Worker's signature check proves authenticity only — that Rails minted this token. Entitlement (active status, quota, subscription) is re-checked by Rails on every single call.

The license-token lifecycle

Everything downstream of the web app is gated by one object: the license token minted at download time. Step by step:

  1. Download. A subscribed account admin requests an ISO from /downloads, choosing a product (engine or hotswap). Rails mints a LicenseToken — an Ed25519 JWS whose jti is the token's UUID — and redirects to a v3-signed download-Worker URL. The Worker verifies the HMAC and streams the ISO, splicing the token into the /averase.key slot. The customer receives a stick that is already bound to their account.

  2. Boot → locked. The kiosk boots into a hard lock: no drive enumeration, no tests, no wipe screen, no hardware report. Only Wi-Fi, retry, and power controls work. Hardware details never leave the device while locked.

  3. Verify (unmetered). Once online, the kiosk calls the engine Worker's POST /api/verify, which forwards to Rails POST /internal/tokens/verify. Rails applies the full entitlement decision but does not meter quota — booting isn't wiping. Only an affirmative allow unlocks the UI, for that boot only. There is no offline unlock.

  4. Asset report. Once verified and connected, the kiosk sends its once-per-boot hardware report (POST /api/record → Rails POST /internal/assets): the complete fingerprint plus internal storage. Rails upserts one asset per physical machine per account (keyed by DMI UUID + serial), and the machine appears in /inventory. Operator notes and hardware-test results attach to the same record.

  5. Check-in (metered, fail-closed). Before any wipe, the kiosk calls POST /api/checkin → Rails POST /internal/tokens/validate. Rails makes the allow/deny decision, atomically increments the token's use_count, and issues a job_id. A deny, an unprovisioned stick, or any network failure blocks the wipe — an unentitled or offline stick is inert.

  6. Wipe result → certificate. After each wipe, the kiosk posts one POST /api/wipe_result per drive. The Worker forwards it to Rails POST /internal/wipe_results (idempotent on job_id), which persists the result and — for successful, verified wipes — enqueues certificate generation: two PDFs signed with Rails' certificate key, protected by an HMAC tamper token, uploaded to private R2. Certificates are made only in Rails; the kiosk just relays the outcome. Anyone can check a certificate at the public GET /certificates/:id/verify endpoint.

  7. Outbound delivery. After persistence, Rails fans events out — webhooks (asset_reported, wipe_session_ready, wipe_session_completed, wipe_session_blocked, hardware_tests_completed, certificate_generated) and ERP integrations — and serves the read-only public API for downstream systems.

  8. Remote kill. Flipping a token's status to revoked or suspended denies its next verify, check-in, wipe result, and asset report — the stick goes dark on its next contact. Account admins do this from the ISO control column on /downloads; a separate staff-only disabled status outranks everything and cannot be undone by the account. See license tokens.

A healthy boot, on the wire

A normal boot-and-wipe session produces this request sequence at the engine Worker:

Request trace

GET  /api/health        # connectivity probe (no auth decision)
POST /api/verify        # unlock the UI — unmetered
POST /api/record        # once-per-boot asset report
POST /api/checkin       # pre-wipe entitlement + quota — metered
POST /api/wipe_result   # one per drive, after the wipe

Every POST carries the device token as a Bearer JWS; every forward to Rails carries the internal-API HMAC. Full request and response shapes are in the engine Worker API and the internal API reference.

What's next?

Was this page helpful?