The API supports two signer backends controlled by SIGNER_MODE in wrangler.toml:
Enclave mode (SIGNER_MODE = "enclave", default): private keys live in a hardware-isolated Evervault Enclave. The operator cannot read keys. Higher cost; requires a separate enclave deployment.
Worker mode (SIGNER_MODE = "worker"): private keys are encrypted at rest in D1 using AES-256-GCM with a WORKER_SEALING_KEY Cloudflare secret. The operator holds the master key and can in principle decrypt any key. Lower cost; no separate service needed.
| Service | Runtime | Domain |
|---|---|---|
| API | Cloudflare Worker (Hono + D1 + KV) | api.clw.cash |
| Web Payment UI | Cloudflare Pages | pay.clw.cash |
| Enclave Signer | Evervault Enclave | clw-cash-signer.app-366535fdf2b7.enclave.evervault.com |
| Service | Runtime | Domain |
|---|---|---|
| API | Cloudflare Worker (Hono + D1 + KV) | api.clw.cash |
| Web Payment UI | Cloudflare Pages | pay.clw.cash |
No separate signer service is deployed. The API Worker signs directly using the WORKER_SEALING_KEY secret.
cd api
npx wrangler d1 create clw-cash-db
# Copy the database_id into wrangler.toml (both default and env.production)npx wrangler kv namespace create KV_TICKETS
npx wrangler kv namespace create KV_RATE_LIMIT
# Copy the IDs into wrangler.toml (both default and env.production sections)Note: Challenges are stored in D1 (not KV) for strong consistency during the auth flow.
# Local
npx wrangler d1 migrations apply clw-cash-db --local
# Production
npx wrangler d1 migrations apply clw-cash-db --env production --remotecd api
echo "<value>" | npx wrangler secret put INTERNAL_API_KEY
echo "<value>" | npx wrangler secret put TICKET_SIGNING_SECRET
echo "<value>" | npx wrangler secret put SESSION_SIGNING_SECRET
echo "<value>" | npx wrangler secret put TELEGRAM_BOT_TOKEN
echo "<value>" | npx wrangler secret put TELEGRAM_BOT_USERNAME
echo "<value>" | npx wrangler secret put EV_API_KEY # enclave mode only
# Worker mode only — generate and set a 32-byte AES-256 master key:
openssl rand -hex 32 | npx wrangler secret put WORKER_SEALING_KEY
# Repeat with --env production for prodIn api/wrangler.toml under [vars] and [env.production.vars]:
# Enclave mode (default — hardware isolation, higher cost)
SIGNER_MODE = "enclave"
# Worker mode (no enclave — operator-trusted, lower cost)
SIGNER_MODE = "worker"Install the Evervault CLI, then:
# one-time: generate signing certs
ev enclave cert new --output ./infra
# build enclave image
ev enclave build -v --output . -c ./infra/enclave.toml ./enclave
# deploy
ev enclave deploy -v --eif-path ./enclave.eif -c ./infra/enclave.tomlcd api
npx wrangler deploy # dev
npx wrangler deploy --env production # prodcurl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://api.clw.cash/telegram-webhook"
# Verify
curl "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"cd web
pnpm build
npx wrangler pages deploy dist --project-name=clw-cash-webOr connect GitHub repo in Cloudflare Dashboard > Pages for auto-deploys.
Add custom domain pay.clw.cash in Dashboard > Pages > clw-cash-web > Custom domains.
# Terminal 1: API Worker (localhost:8787)
cd api
npx wrangler d1 migrations apply clw-cash-db --local
npx wrangler dev
# Terminal 2: Enclave signer (localhost:7000)
pnpm --filter ./enclave start
# Terminal 3: Web UI (localhost:5173, talks to localhost:8787)
cd web
pnpm devSet SIGNER_MODE = "worker" in wrangler.toml [vars] and add WORKER_SEALING_KEY to api/.dev.vars:
WORKER_SEALING_KEY=<output of: openssl rand -hex 32>Then only two terminals needed:
# Terminal 1: API Worker (localhost:8787)
cd api
npx wrangler d1 migrations apply clw-cash-db --local
npx wrangler dev
# Terminal 2: Web UI
cd web
pnpm devWhen TELEGRAM_BOT_TOKEN is not set, the API runs in test mode: challenges auto-resolve when a telegram_user_id is provided in the challenge request body.
Vars (in wrangler.toml):
SIGNER_MODE—"enclave"(default) or"worker"ENCLAVE_BASE_URL— enclave signer URL (enclave mode only)ALLOWED_ORIGINS— comma-separated CORS origins (e.g.https://pay.clw.cash)TICKET_TTL_SECONDS,SESSION_TTL_SECONDS,CHALLENGE_TTL_SECONDSRATE_LIMIT_WINDOW_MS,RATE_LIMIT_PER_USER,RATE_LIMIT_PER_IDENTITY_SIGN
Secrets (via wrangler secret put):
INTERNAL_API_KEY— shared secret with enclave (enclave mode only)TICKET_SIGNING_SECRET— JWT signing for ticketsSESSION_SIGNING_SECRET— JWT signing for sessionsTELEGRAM_BOT_TOKEN— from @BotFather; omit to enable test modeTELEGRAM_BOT_USERNAME— bot username without @EV_API_KEY— Evervault API key (enclave mode only)WORKER_SEALING_KEY— 32-byte hex AES-256 key for encrypting private keys (worker mode only)
Keys at rest in key_backups use different formats per signer mode.
| Mode | Format | Notes |
|---|---|---|
| Enclave (Evervault) | Evervault ciphertext | Decryptable only inside the enclave |
| Enclave (local fallback) | {iv}:{ciphertext}:{tag} |
Node.js AES-256-GCM, 3 parts |
| Worker | {iv}:{ciphertext+tag} |
WebCrypto AES-256-GCM, 2 parts |
Sealed keys from one mode are not interchangeable with the other. Do not switch modes for an existing deployment without re-sealing all keys.
VITE_API_URL— API Worker URL (set in.env.developmentand.env.production)
API domain: edit pattern in api/wrangler.toml:
[[env.production.routes]]
pattern = "api.clw.cash/*" # ← change here
zone_name = "clw.cash" # ← and here if different zoneWeb domain: change in Cloudflare Dashboard > Pages > Custom domains.
API URL for web: update web/.env.production:
VITE_API_URL=https://api.clw.cash
curl https://api.clw.cash/health
# {"ok":true,"service":"api"}- Auth challenge:
POST /v1/auth/challenge→challenge_id+deep_link - User opens
deep_linkin Telegram, taps Start (webhook resolves challenge) - Verify:
POST /v1/auth/verify→ sessiontoken+user - Create identity:
POST /v1/identities - Sign:
POST /v1/identities/:id/sign-intentthenPOST /v1/identities/:id/sign - Destroy:
DELETE /v1/identities/:id - Audit:
GET /v1/audit
# Real-time Worker logs
npx wrangler tail
npx wrangler tail --env production
# Query D1
npx wrangler d1 execute clw-cash-db --env production --remote --command="SELECT count(*) FROM users"
# Inspect KV
npx wrangler kv key list --namespace-id=<ID>-
Signing fails with key-not-found after enclave restart (enclave mode): API auto-restores from D1 backup and retries.
-
Signing fails with "No key backup found" (worker mode): the
key_backupsrow is missing for this identity. The key cannot be recovered — it was lost during a failed identity creation. -
Signing fails with decryption error (worker mode):
WORKER_SEALING_KEYmay have been rotated without re-sealing existing keys. See Master Key Rotation below. -
Ticket replay errors: verify clients are not reusing old sign tickets; check clock sync.
-
Telegram webhook not working:
curl https://api.telegram.org/bot<TOKEN>/getWebhookInfo- Check Worker logs:
npx wrangler tail - Re-set webhook URL if needed.
-
CORS errors on web UI: verify
ALLOWED_ORIGINSinwrangler.tomlincludeshttps://pay.clw.cash.
NEW_SECRET=$(openssl rand -hex 32)
echo "$NEW_SECRET" | npx wrangler secret put SESSION_SIGNING_SECRET --env production
# Repeat for TICKET_SIGNING_SECRET, INTERNAL_API_KEYRotating WORKER_SEALING_KEY invalidates all existing sealed keys in D1. Before rotating:
- Export all
sealed_keyvalues fromkey_backups - Decrypt each with the old key, re-encrypt with the new key, update D1
- Set the new
WORKER_SEALING_KEYsecret
There is no automated tool for this yet — write a migration script before rotating.
When switching a live deployment from enclave mode to worker mode, existing identities cannot be reused — their sealed_key in D1 is an Evervault ciphertext that the worker signer cannot decrypt.
The API detects this automatically: POST /v1/identities/:id/restore returns 409 if the stored key is not in worker format.
cash init (v0.1.28+) handles the 409 transparently:
- Detects the incompatible identity
- Calls
DELETE /v1/identities/:idto mark it destroyed - Creates a fresh worker-mode identity and saves config immediately
Funds at the old public key are inaccessible — the private key lived in the Evervault enclave. Recover any funds before switching modes.
Users must run cash init (not cash login) after a mode switch. cash login only refreshes the session token and does not handle identity migration.
# Rollback Worker
npx wrangler rollback --env production
# Rollback Pages: Dashboard > Pages > clw-cash-web > Deployments > RollbackIf migrating from the old in-memory Express API:
# Export key backups from old api-data/key-backups.json and import to D1
npx wrangler d1 execute clw-cash-db --env production --remote --file=migration.sql- Replace plaintext sealed_key backup with encrypted backup (done — Evervault / AES-256-GCM in both modes)
- Worker mode: pure-Cloudflare key management without enclave dependency
- Add SIEM export for audit events (Cloudflare Logpush)
- Set up monitoring alerts (Workers Analytics + PagerDuty/Sentry)
- Enforce attestation verification policy in callers (enclave mode)
- Master key rotation tooling (worker mode)