Public edge receiver for CiviCRM webhooks, part of the door-sync system. It sits between CiviCRM (which fires a signed webhook via a CiviRules custom action) and the door-sync daemon on a Raspberry Pi behind NAT.
CiviCRM (CiviRules action) ──HMAC POST──▶ door-webhook (this Worker)
│ validate + buffer on a Queue
▼
Queue consumer ──HMAC POST + CF Access──▶
door-sync (Pi, via Cloudflare Tunnel)
fetchverifies the request signature (CIVICRM_WEBHOOK_SECRET), normalizes it to aWebhookEvent, sends it to theEVENTSqueue, and returns202immediately. A bad/missing/stale signature returns401.queueconsumer pushes each event to the Pi at${ORIGIN_URL}/civicrm/membership-changed, re-signing withORIGIN_HMAC_SECRETand presenting a Cloudflare Access service token. A non-2xx response retries; aftermax_retriesthe message is dead-lettered (door-webhook-dlq) rather than dropped — so a Pi reboot never loses an event.
Both hops use the same scheme (shared with the Pi's Python receiver):
X-Door-Sync-Timestamp: <unix seconds>
X-Door-Sync-Signature: sha256=<hex>
hex = HMAC_SHA256(secret, `${timestamp}.` + rawBody)
The verifier rejects a timestamp more than 300s from now (replay guard).
wrangler.jsonc holds no account id, no hostname and no secrets, so it is safe
to publish. The account comes from a git-ignored .env, secrets from
wrangler secret put, and ORIGIN_URL from the Cloudflare dashboard —
keep_vars: true is what stops each deploy from deleting it.
npm install
# 1. Point the CLI at the right Cloudflare account. Without this wrangler
# refuses to guess when your login can reach more than one account.
cp .env.example .env # then set CLOUDFLARE_ACCOUNT_ID (npx wrangler whoami)
# 2. Create the queues (once per account):
npx wrangler queues create door-webhook-events
npx wrangler queues create door-webhook-dlq
# 3. Set ORIGIN_URL in the Cloudflare dashboard (Worker -> Settings -> Variables)
# to the Pi's Cloudflare Tunnel hostname — scheme + host only, the Worker
# appends the path. It is a plain var, not a secret, so its value stays
# readable there; `keep_vars` in wrangler.jsonc keeps deploys from wiping it.
# 4. Set the secrets:
npx wrangler secret put CIVICRM_WEBHOOK_SECRET # shared with the CiviRules action
npx wrangler secret put ORIGIN_HMAC_SECRET # shared with the Pi (WEBHOOK_HMAC_SECRET)
npx wrangler secret put CF_ACCESS_CLIENT_ID # Access service token for the tunnel
npx wrangler secret put CF_ACCESS_CLIENT_SECRET
# 5. Deploy:
npm run deployThe queue consumer checks all of these before it signs a delivery, so a missing
one is logged by name (missing Worker config: ORIGIN_URL, ...) rather than
surfacing as a crypto error.
For local development, copy .dev.vars.example to .dev.vars (git-ignored) and
fill it in, ORIGIN_URL included — wrangler types reads that file to type the
bindings, so re-run npm run cf-typegen after adding a key there or changing
wrangler.jsonc. Use .dev.vars rather than .env for these: if both exist,
.dev.vars wins and Worker bindings in .env are silently ignored. .env is
only for the wrangler CLI's own variables, such as CLOUDFLARE_ACCOUNT_ID.
| Command | Purpose |
|---|---|
npm run dev |
Local dev server at http://localhost:8787 |
npm test -- run |
Run the Vitest suite once |
npm run deploy |
Deploy to Cloudflare |
npm run cf-typegen |
Regenerate worker-configuration.d.ts from wrangler.jsonc |