Edge-First Signal Verification
What the Cloudflare Worker in front of api.passkeybridge.io does, which headers it forwards, and which checks run at the origin.
Overview
api.passkeybridge.io is a Cloudflare Worker in front of the Supabase edge functions. The split is deliberate and worth knowing before you debug a 401 or a 404: the Worker terminates TLS, applies the WAF and the per-IP rate limit, refuses function names it does not know, and forwards a fixed list of headers. Everything that touches a key, a secret or the database happens at the origin, in the function.
The Worker is stateless and carries no tenant material: it holds one shared secret that proves a request came through it, and nothing else. HMAC verification of an inbound signal, API key lookup, plan and quota checks, identifier hashing, playbook matching and the audit write all run at the origin in shield-ingest.
One consequence to plan around. A header has to be on the Worker's forward list to be usable at the public host; everything else is dropped before the function runs. The list carries eighteen names, x-pb-signature, x-shield-signature, dpop and if-none-match among them, so an inbound signature reaches the origin, an organization with Require signed signals switched on works through the public host, a DPoP proof is observed, and a conditional status-list read can answer 304. The full list is in the next section.
Shipped architecture
At the edge, in order:
OPTIONSis answered 204 with the CORS headers./robots.txtdisallows everything, and every response carriesX-Robots-Tag: noindex./and/healthreturn the Worker version and isolate-local proxy counters without calling upstream./health?deep=1makes a real proxied round trip to a harmless upstream function and reports which layer failed./v1/_debug/echoanswers at the edge with what arrived: the method, the path, whether an API key and an authorization header were present, the first 8 characters of the key, and the Cloudflare ray id. This is the only endpoint that echoes request shape, and it never proxies.- The first path segment is checked against the public function allowlist. Anything else is 404 with
x-pb-reason: unknown-function, without touching upstream. - The allow-listed headers are copied onto a new request, the client IP is added as
X-Forwarded-ForandX-Real-IP, any client-suppliedx-pb-edge-secretis deleted, and the real one is injected. - The upstream response is returned with CORS and security headers, an
x-pb-debug-idcarrying the ray id, and, on a 401, 403 or 429 that arrived without one,x-pb-reason: upstream-auth. A transport failure is 502 withx-pb-reason: upstream-unreachable.
Headers the Worker forwards, eighteen in all: authorization, content-type, apikey, stripe-signature, x-client-info, x-pb-api-key, x-shield-api-key, x-pb-tenant-id, x-pb-test-mode, x-pb-signature, x-shield-signature, dpop, if-none-match, x-okta-verification-challenge and the four Supabase client-info headers. Anything else you send is dropped.
At the origin, shield-ingest refuses any request that did not carry the edge secret with 403 Direct access denied, then authenticates the key or the test-mode session, verifies a signature if one arrived, enforces the plan rate limit and the caps, hashes the identifier under the server-held pepper, matches one playbook, runs the hard or soft response and the actions, and writes the event and audit rows.
client -> Cloudflare Worker -> shield-ingest
TLS, WAF, per-IP limit, edge-secret check, API key, scopes,
allowlist, header forward, signature, quota, hashing, playbook,
edge-secret injection revocation, actions, event, audit
stateless, no tenant material every key and every rowOperation placement today
| Operation | Layer | Notes |
|---|---|---|
| TLS termination | Cloudflare | Nearest data centre |
| WAF and bot mitigation | Cloudflare | Codified in desired-state.json, applied by shield-waf-reconcile, drift-checked nightly |
| Per-IP rate limiting | Cloudflare | 1,200 requests per 60 seconds per source IP per data centre, then a 60-second block |
| Function allowlist | Cloudflare Worker | Unknown names 404 with x-pb-reason: unknown-function |
| Header filtering and edge-secret injection | Cloudflare Worker | Fixed forward list; client-supplied edge secrets are stripped |
| Edge-lock enforcement | Origin | 403 Direct access denied without the injected secret |
| API key lookup and scope check | Origin | Digest lookup in shield_api_keys |
| Inbound HMAC verification | Origin | Against the current secret, or the previous one for 24 hours after a rotation |
| Per-organization rate limit | Origin | Two layers, plan limit per organization per source IP |
| Entitlement, monthly cap, sandbox ceiling | Origin | Plan, billing status and unmetered allowance |
| Identifier hashing | Origin | HMAC-SHA-256 under a server-held pepper |
| Playbook matching and actions | Origin | One playbook, actions in parallel |
| Hard and soft response | Origin | Revocation or trust evaluation, before the actions |
| Event row and audit write | Origin | shield_events and shield_audit_log |
Replay protection is not on this path. Nonce-based replay checking belongs to the A2A handshake, which has its own nonce table and clock-drift rules. Ingest has no nonce: a signal replayed with the same body is accepted and stored again, and deduplication is your side's job, or the correlation id's.
Rule of thumb. If it needs a key, a secret, the plan or the database, it runs at the origin. The Worker's whole state is one shared secret and a per-isolate counter it reports on /health.
Measured latency
There is no published figure for this path in this guide, and that is deliberate: a number restated in several places is a number that goes stale in several places.
What you can measure yourself. The playground at /developers/playground calls the API from your own browser and reports what it observes, live. Whatever it shows is a measurement of your network path to the nearest Cloudflare data centre and back; it is no platform promise.
What the platform records per signal. Every event row carries two figures. decision_ms covers PasskeyBridge's own work: authentication, organization lookup, identifier hashing and playbook matching, up to the decision. response_ms covers the whole request, including outbound playbook actions to Slack, your webhook receiver and Resend. They are kept apart because conflating them made an organization's own chart degrade whenever a third party was slow.
Both are shown per event in Dashboard > Events, on the row and in the drawer, and the decision-latency distribution is available in the Observability tab. Read your own numbers there rather than inferring them from a published figure.
Audit log consistency
Verification and the audit write both happen at the origin, inside one request, so there is no window in which a signal has been verified but not recorded.
Every accepted signal writes a shield_events row, whether or not a playbook matched, and a shield_audit_log row with action signal.ingest carrying the playbook outcome, the action count, the failure count, the test-mode and sandbox flags and the correlation id. A hard signal writes a second audit row, parametric_revocation.triggered, with the revocation counts. Both are scoped to the organization by row-level security.
The correlation id is generated inside shield-ingest, returned in the response, stored in the event's metadata, written to the audit rows and forwarded to the trust engine and the intelligence worker as x-pb-correlation-id. One id therefore joins the verification, the playbook execution, the outbound deliveries and any later assessment.
Two writes are deliberately not in that guarantee. The usage counter and the audit row are written after the event row and only if it succeeded, so a failed insert answers 502 rather than billing for a signal it did not store. And webhook retries happen later, on the worker, so a delivery row can appear minutes after the event row it belongs to.
Moving verification to the edge
Verifying the inbound HMAC at the Cloudflare data centre would reject a forged signal before it costs an origin invocation. It is not shipped, and two things would have to exist first.
A way to get per-organization secrets to every data centre. The Worker holds no tenant material today, which is what makes it safe to treat as untrusted infrastructure. Distributing signing secrets to it means a push-and-revoke path, a rotation story that keeps the 24-hour grace window, and a compromise story for the edge itself.
A validator at the edge equal to the one at the origin. The signature is over the raw body, and the body is only meaningful after the schema check that runs in the function. Verifying a signature over a payload that the origin will then reject buys nothing.
Until then, the answer is: TLS, WAF and rate limiting at the edge; authentication, verification, tenancy and playbooks at the origin.
Limits of the edge layer
The edge stops volume and unknown paths. It cannot make the decisions that need state.
- Anything about a specific organization. Plan, quota, monthly cap, API key scopes, signing secrets and the playbook set all live in the database, so the request has to reach the origin before any of them can be applied. A per-IP block at the edge is not a per-organization limit and does not replace one.
- Correlating several signals. A response that depends on what else this organization has seen recently reads
shield_events, which the Worker cannot query. - Anything carried in a header outside the forward list. A custom header of your own, and an inbound
x-pb-correlation-id, are dropped before the function can see them. Signatures and DPoP proofs are on the list and do arrive. - Data residency. A globally distributed edge decides nothing about where a verification event is stored; that is a property of the origin and its database.
In each case the shape is the same today: the Worker terminates TLS, applies the WAF and the rate limit, and the origin makes the decision.
Related from the blog
- How to Benchmark an Identity Verification API: Latency, Freshness, Failure Injectionengineering · 12 min read
- Account Recovery Is the Front Door: Carrier-State Checks in Credit Union and Community Bank Recovery Flowssecurity · 11 min read
- Carrier Signal Fusion: Why Single-Provider Lookups Are a Single Point of Failuresecurity · 14 min read