cd ..

Edge API Gateway & Serverless Subscription Backend

Runtime: Cloudflare Workers (V8 isolates) | Role: Backend / Edge Engineering | Pattern: Zero-server, globally-distributed API
Cloudflare Workers TypeScript Cloudflare KV Cloudflare D1 Firebase / JWKS Stripe Webhooks Jose (JWT) Vitest

Traditional API architectures place a server — or a cluster of servers — between the client and its data. Those servers live in a region. Users far from that region pay a latency penalty on every request. Scaling requires provisioning, and idle hours cost money.

I built a two-layer edge backend that eliminates the server entirely. An API gateway and a subscription management backend run as Cloudflare Workers — V8 isolates deployed simultaneously across 300+ edge locations worldwide. There is no origin server to scale, no region to pick, and no idle infrastructure cost.

300+ Edge Locations
0ms Cold Start (V8 isolate)
0 Origin Servers
KV Single Source of Truth
Firebase Auth via JWKS
410 Gone for Dead Endpoints

Layer 1: The API Gateway

Every inbound request hits the gateway Worker first. Its job is singular and non-negotiable: verify identity before forwarding anything.

Client Request
    │
    ▼
[ Cloudflare Gateway Worker ]
    │
    ├──► Extract Authorization: Bearer <firebase_jwt>
    │
    ├──► Fetch JWKS from Firebase ──► Verify RS256 Signature
    │
    ├──► Validate: expiry, issuer, audience
    │
    ├──► Inject X-Authenticated-UID header
    │
    ▼
[ Internal Microservice ] (never sees raw JWT — only the verified UID)

Why JWKS instead of shared secrets?

Firebase rotates its signing keys automatically. A gateway that hardcodes a public key will break when Firebase rotates. By fetching the JWKS endpoint and selecting the correct key by kid (Key ID) from the JWT header, the gateway remains valid across rotations with zero operator intervention. The Jose library handles the cryptographic verification; the Worker handles the routing logic.

Identity propagation without re-verification

Once the gateway verifies the JWT, it strips the token and injects the verified user ID as an X-Authenticated-UID header. Downstream services never receive an unverified token — they receive a pre-verified identity claim. This eliminates per-service JWT verification and keeps the auth boundary at the edge.

Layer 2: The Subscription Backend

The backend Worker handles subscription state for a Flutter application — managing active subscriptions, trial periods, and Stripe payment webhooks. The architectural constraint was strict: every read must be fast enough to not block app startup, and every write must be atomic enough to never leave the user in an inconsistent state.

Cloudflare KV as the single source of truth

KV (Key-Value store) is globally replicated with low read latency. The subscription state for each user is a single KV key. The read path is a direct KV lookup — no database query, no JOIN, no cache miss. The write path is a single KV put, triggered by a verified Stripe webhook event.

Design decision: Cloudflare D1 (SQL database) exists in the system but only for audit history — appending an immutable record of each subscription event for compliance and debugging. D1 is never on the hot read path. If D1 is down, the app still works. If KV is down, nothing works — so KV is treated as the reliability boundary.

Stripe Webhook → State Machine

Stripe fires webhook events for every subscription lifecycle transition. The Worker consumes these events and maps them to KV writes. The state machine is intentionally simple — complexity in payment state machines is where bugs hide.

checkout.session.completed → KV: status=active, plan=pro
customer.subscription.updated → KV: update plan tier + expiry
customer.subscription.deleted → KV: status=cancelled
invoice.payment_failed → KV: status=past_due
invoice.paid (renewal) → KV: status=active, extend expiry

Request Lifecycle: End-to-End

1
Client sends Firebase JWT in Authorization header Flutter app uses Firebase Auth SDK — the JWT is issued by Firebase after phone/email sign-in
2
Gateway fetches Firebase JWKS (cached) The JWKS is cached at the Worker level for 6 hours — a balance between key rotation latency and upstream pressure
3
Gateway verifies JWT signature, expiry, and issuer RS256 verification against the matching public key from JWKS. Rejection returns HTTP 401 immediately
4
Gateway injects X-Authenticated-UID and forwards The raw JWT is stripped. The verified UID is forwarded. The backend service trusts the UID without re-verification
5
Backend reads subscription state from KV Single KV lookup by UID. Returns active/trial/expired + plan tier. Response time is sub-millisecond at the edge

Dead Endpoint Hygiene

APIs accumulate dead routes over time. Most backends return 404 for missing routes — but 404 implies the resource might exist elsewhere or in future. For removed endpoints, the correct HTTP status is 410 Gone: the resource existed here and has been permanently removed. Clients and CDN caches can treat 410 as a signal to stop retrying.

Implementation detail: Every explicitly removed endpoint in this backend returns 410 with a JSON body explaining the removal. This is a small discipline with outsized benefit — it prevents clients from retrying dead endpoints indefinitely and makes the API's evolution legible.

Local Development & Testing Strategy

Cloudflare Workers run on V8 isolates, not Node.js. The runtime exposes Web APIs (Fetch, Cache, KV bindings) but not Node.js globals. Testing required a Vitest-based suite with mocked Worker bindings rather than a local Express server masquerading as a Worker.

Gateway Test Strategy

The gateway test suite mocks the JWKS endpoint and tests: valid JWT acceptance, expired JWT rejection, wrong issuer rejection, missing header rejection, and correct UID header injection. Local mode bypasses JWT verification (non-production only).

Backend Test Strategy

The subscription Worker tests mock KV bindings and Stripe webhook payloads. Each state transition is tested in isolation. Webhook signature verification is tested with known test secrets from Stripe's test mode.

Engineering Tradeoffs

KV consistency model

Cloudflare KV is eventually consistent — a write in one region propagates to other regions within seconds, not milliseconds. For subscription state, this means a user who just upgraded on the US-East edge might see their old state from the EU edge for a few seconds. This is an acceptable tradeoff for a subscription product — users don't upgrade mid-second and expect simultaneous global consistency. If this were a financial ledger, the architecture would use Cloudflare Durable Objects (strongly consistent) instead.

Stateless design eliminates drift

Because the Workers hold zero in-memory state between requests (V8 isolates are ephemeral), there is no risk of stale state accumulating over time. Every request starts from the authoritative KV store. There is no cache invalidation problem because there is no cache — just an eventually consistent global KV.