← Selected work

Case study / 03 / Payments & entitlements

Keeping subscription access correct across Apple and Stripe.

A multi-provider subscription system that verifies signed billing events, reconciles provider state, and maintains one application-owned view of a user’s access.

Why subscriptions are a systems problem

Payment providers do not share a single lifecycle or data model. Apple App Store subscriptions and Stripe billing each send their own events, identifiers, signatures, and status transitions. The application still needs to make one clear decision: does this person currently have access?

That decision cannot depend on a browser redirect or a single successful checkout response. Subscription changes arrive asynchronously, delivery can be repeated, and the provider’s current state may contain more context than one notification. I designed the billing boundary so external events are verified, recorded, and translated into durable application state before they can affect access.

A secure, idempotent event boundary

Stripe webhooks are verified using the provider signature before an event is handled. Apple notifications are verified and decoded with the App Store server library before processing begins. The webhook endpoints remain deliberately small: their job is to authenticate an external event and pass it into a transactional handler, rather than mixing transport concerns with subscription rules.

Both event paths use advisory locks keyed by the provider event ID. Inside that lock, the service checks an event ledger before proceeding and records the processed event in the same transaction. A repeated notification therefore becomes an acknowledged no-op instead of another subscription mutation. Database serialization errors are retried a bounded number of times, which keeps contention from becoming a user-facing billing failure.

Reconcile provider facts, then derive access

Stripe subscription lifecycle events retrieve the current subscription object from Stripe before mapping it into local records. Apple processing does more than trust the incoming notification: it retrieves the current subscription snapshot from Apple using the original transaction ID, upserts the local subscription and transaction records together, and links the subscription to the user when the verified account token allows it. That gives the application a current, normalized record of renewal, expiry, revocation, trial, and product state.

From either provider, the system derives a common user-subscription record with a source, status, renewal time, expiry, and next renewal time. The entitlement layer explicitly guards cross-provider conflicts: it does not replace an active subscription from one provider with an incoming update from another, and it avoids letting an inactive Apple update overwrite a different active Apple entitlement. Each change also updates the application’s subscription flag and invalidates cached access state.

Why the separation matters

Provider-specific code owns verification and provider-state mapping. The application owns the canonical user entitlement and the rules for updating it. That division makes it possible to support Apple and Stripe without spreading payment-provider assumptions through the rest of the product.

It also gives failures a clear place to live. Signature validation rejects untrusted requests, event ledgers make duplicate delivery visible, transactions prevent partial writes, and normalized entitlement state gives the rest of the platform one stable interface for deciding access. This is the kind of backend work where correctness is directly tied to user trust and revenue.