Self-Hosting Guide
Self-hosting runs the HumanAuth platform — the authorization server that routes requests to humans, verifies device co-signatures, and mints receipts — inside your own Cloudflare account. Approval data, tenant signing keys, and audit history never leave infrastructure you control, and receipts stay verifiable with the standard open verifier. Self-hosting is licensed and supported under an Enterprise agreement.
What you deploy
Section titled “What you deploy”One Worker plus the managed primitives it binds:
| Piece | Cloudflare primitive | Purpose |
|---|---|---|
| Platform API | Worker (packages/platform) | /v1/* — authorize, respond, receipts, JWKS, webhooks |
| State | D1 database | Tenants, humans, devices, requests, receipts, signing keys, audit |
| Webhook delivery | Queue + dead-letter queue | Async HMAC-signed event delivery with retries |
| Brand assets | R2 bucket | Tenant logo uploads |
| Rate limiting | Workers rate-limit bindings | Per-tenant authorize / per-human respond ceilings |
| Sweeps | Cron trigger (1/min) | Expiry flips, stranded webhook redelivery |
| Dashboard (optional) | Pages (packages/dashboard) | Admin UI — point it at your Worker |
Not in this list, deliberately: the verifier and replay store, which run at your resource server in every deployment model, and the approver apps, which are distributed builds (see Limitations).
The verifier and replay store when self-hosting
Section titled “The verifier and replay store when self-hosting”Two facts evaluators usually come here for:
The replay store doesn’t move — it was never ours to host. Self-hosting relocates the platform (issuance) into your Cloudflare account; the replay store always lives with your resource server, in hosted and self-hosted deployments alike. Set it up exactly as the Verifier SDK guide describes — the Redis/Postgres/SQLite/Workers KV adapters, the auto-provisioned schema, and the operations notes on pruning, fail-closed availability, and failover all apply unchanged.
The verifier needs exactly two settings: your JWKS and your issuer.
const verifier = new HumanAuthVerifier({ audience: "tn_your_tenant", issuer: "https://approvals.example.com", // = HA_ISSUER below jwksUri: "https://approvals.example.com/.well-known/jwks.json", // your platform Worker replayStore: new RedisReplayStore(redis), // yours either way});Air-gapped enforcement can pass an inline jwks snapshot instead of jwksUri. Cross-acceptance between deployments is closed twice over: signatures only verify against your JWKS (every kid is tenant-scoped), and the pinned issuer rejects receipts minted by any other deployment.
Prerequisites
Section titled “Prerequisites”- A Cloudflare account on Workers Paid (Queues requires it), with
wranglerlogged in (npx wrangler login). - Node 20+ and pnpm;
opensslon PATH for key generation. - Pick your auth mode — the platform refuses to boot without an explicit choice:
operator(the bare tier — start here). No identity provider anywhere. Approvers enroll via single-use invite codes you mint from the CLI, and their device keys are the credential from then on. You are the identity authority: handing the code to the right person is the identity ceremony, and offboarding is deactivating the roster row.workos— bring a WorkOS AuthKit application; dashboard sign-in and IdP-based enrollment authenticate through it (this is how the hosted platform runs).
- The platform source, under your Enterprise agreement.
Deploy the platform
Section titled “Deploy the platform”The reference implementation of everything below is packages/platform/scripts/provision-prod.sh — a provisioning script you can read top to bottom and adapt. The manual sequence:
1. Point wrangler.toml at your account. In packages/platform/wrangler.toml, replace the production [[routes]] block with your own domain and zone (or set workers_dev = true while evaluating), and prepare to paste your D1 id in the next step. The [env.staging] block shows the same pattern for a pre-production environment — recommended, so migrations and key rotation get a rehearsal target.
2. Create the database and apply migrations.
cd packages/platformnpx wrangler d1 create humanauth-prod # paste the id into wrangler.tomlnpx wrangler d1 migrations apply humanauth-prod --remoteMigrations are ordered SQL files in migrations/; migrations apply is also the upgrade path later.
3. Create the queues and buckets.
npx wrangler queues create humanauth-webhook-deliveriesnpx wrangler queues create humanauth-webhook-deliveries-dlqnpx wrangler r2 bucket create humanauth-brand-assetsnpx wrangler r2 bucket create humanauth-brand-assets-preview4. Generate and set the secrets. Three platform keys — plus the WorkOS trio only if you chose workos mode:
openssl rand -hex 32 | npx wrangler secret put SIGNING_PRIVATE_KEYopenssl genpkey -algorithm ed25519 | npx wrangler secret put HA_SIGNING_KEY_PEMopenssl rand -hex 32 | npx wrangler secret put HA_KEK_V1_HEXnpx wrangler secret put WORKOS_CLIENT_ID # from your WorkOS dashboardnpx wrangler secret put WORKOS_API_KEYnpx wrangler secret put WORKOS_WEBHOOK_SECRETHA_KEK_V1_HEX is the key-encryption key that activates per-tenant signing keys (each tenant’s receipts sign under its own kid, stored AES-256-GCM-encrypted in D1). HA_SIGNING_KEY_PEM + the HA_SIGNING_KEY_KID var are the platform-wide fallback. Keep offline copies of all three in your secrets manager — rotation runbook: packages/platform/docs/operations/key-rotation.md.
5. Declare your mode and issuer. In wrangler.toml [vars]:
AUTH_MODE = "operator" # or "workos" — explicit, always; partial config refuses to bootHA_ISSUER = "https://approvals.example.com"Every receipt your deployment mints carries this as its iss claim; every verifier you run pins the same value in its issuer option. Left unset, receipts claim the hosted platform’s https://api.humanauth.ai — functional, but wrong provenance for a self-hosted deployment; set it before minting anything you intend to keep.
6. Deploy and smoke-test.
npx wrangler deploycurl https://approvals.example.com/health # {"status":"ok",...}curl https://approvals.example.com/.well-known/jwks.jsonOperator-mode onboarding — the whole loop
Section titled “Operator-mode onboarding — the whole loop”With AUTH_MODE=operator deployed, enrolling your first approver is three commands and a scan:
humanauth invite create cfo@example.com --qr # mints a single-use code, renders the connect QRThe approver scans it from the HumanAuth app’s Connect to your organization first-run (shipping in the next app release; until then, any client implementing the documented enrollment call — POST /v1/invites/redeem with a freshly generated device key — enrolls the same way). Redemption atomically claims the code, creates the human and roster entry, registers the device public key, and returns a device session. From there the standard loop applies unchanged:
humanauth test payments:transfer # fires a real approval requesthumanauth verify <jws> --issuer https://approvals.example.comNo IdP, no HumanAuth account, no third-party cloud — approve with a biometric, verify offline. Invites are audited (invite.created / invite.redeemed / invite.revoked), listable (humanauth invite list), and revocable before redemption.
Create your first tenant
Section titled “Create your first tenant”(workos mode) Deploy the dashboard (packages/dashboard, an Astro app built for Cloudflare Pages) and give it two things: your WorkOS AuthKit settings (WORKOS_CLIENT_ID, WORKOS_API_KEY, WORKOS_REDIRECT_URI) and your platform URL (PLATFORM_BASE_URL). Sign in, and the dashboard walks you through self-serve tenant creation (POST /v1/tenants) — one call creates the tenant, your owner roster entry, and the first API key, shown in full exactly once. From there: register approver devices from the apps, add humans and groups, and run the 5-minute quickstart against your own URL.
Point everything at your deployment
Section titled “Point everything at your deployment”Every client takes a base-URL override; the hosted URL is only a default:
| Piece | Setting |
|---|---|
@humanauth/sdk | new HumanAuth({ apiKey, baseUrl: "https://approvals.example.com" }) |
@humanauth/verifier | issuer + jwksUri (see above) |
@humanauth/cli | baseUrl in the CLI config file |
@humanauth/mcp | base-URL environment override — see its README |
| Dashboard | PLATFORM_BASE_URL on the Pages project |
| Webhooks | Your subscriptions, delivered from your Worker via your queue |
Availability model
Section titled “Availability model”What “highly available” means here, honestly stated:
- Compute has no single home. The Worker runs in every Cloudflare data center; there is no region to fail over from, no instance count to manage, no server you patch.
- State is the one single-writer core. D1 is a managed, durable, single-primary database — not multi-region active-active. It is the availability bottleneck of the control plane: if D1 is unreachable, new approvals and receipt minting pause. Point-in-time recovery (Time Travel) plus your own
wrangler d1 exportsnapshots cover durability. - Enforcement survives platform outages by design. Verification is offline against cached JWKS — a platform outage can never make an already-minted receipt unverifiable, never blocks your resource servers, and never fails open. The blast radius of total platform downtime is precisely: new approvals stall until it returns (and a request whose TTL lapses during an outage is simply re-asked).
- Webhooks are at-least-once. Deliveries flow through a Cloudflare Queue with five platform retries, app-level retry accounting in D1 with backoff, a per-minute sweep for stranded deliveries, and a dead-letter queue whose depth is itself a monitoring signal.
The design bias, consistent with the rest of the system: the moment that must never break (enforcement) depends on nothing here; the moment that can tolerate a pause (asking a human) degrades to waiting, never to unproven actions.
Backups and upgrades
Section titled “Backups and upgrades”- Backups: scheduled
wrangler d1 exportto your own storage, on top of D1 Time Travel. The secrets (HA_SIGNING_KEY_PEM,HA_KEK_V*_HEX,SIGNING_PRIVATE_KEY) live only in Workers Secrets — keep the offline copies from step 4 current, or minted receipts outlive your ability to rotate gracefully. - Upgrades: pull the release,
npx wrangler d1 migrations apply … --remote,npx wrangler deploy— rehearsed against the staging environment first. Migrations are additive-only in practice; releases note any exceptions.
Security considerations
Section titled “Security considerations”- The Worker terminates TLS at Cloudflare; there is no origin server to harden.
/.well-known/jwks.jsonis public by design — it’s the verification root, not a secret.- API keys can create requests but can never approve them; approvals require registered devices with hardware-held keys, same as hosted.
- KEK and signing-key rotation follow
docs/operations/key-rotation.md(versioned KEKs, re-encrypt, retire). - The replay stores at your resource servers are part of your enforcement security posture — see operations.
Current limitations
Section titled “Current limitations”- Approver apps target the hosted API. The store builds carry two hosted bindings: the platform base URL (Mac: compile-time in
Config.swift; iOS: build-timeEXPO_PUBLIC_PLATFORM_URL) and the WorkOS AuthKit client the sign-in flow authenticates against — a self-hosted platform runs your WorkOS environment, so the hosted apps’ sign-in tokens would be rejected even if the URL matched. No custom implementation is needed, though: the same app codebases rebuild against your URL + your WorkOS client id, distributed through your Apple Business Manager or TestFlight — an Enterprise onboarding item today, with a first-run “connect to your organization” step on the roadmap so one store build serves every deployment. The cryptography is already deployment-agnostic: device keys are generated on-device and registered with whichever platform they pair to. - Push delivery is on the roadmap; approver apps currently learn of pending requests by polling, which self-hosting does not change.
- One platform, many tenants: a self-hosted deployment is multi-tenant exactly like the hosted one; run one deployment per isolation domain, not per team.
See also
Section titled “See also”- Architecture — the three-party topology self-hosting relocates one corner of
- Verifier SDK — enforcement setup, unchanged by self-hosting
- Security Model — what the receipts prove, regardless of who hosts