Skip to content

AppToProduction/stripe-entitlements-rescue-reference

Repository files navigation

Billing Webhook + Entitlement Rescue Reference

CI

A deterministic TypeScript reference for repairing webhook-driven subscription access when deliveries can be forged, duplicated, delayed, reordered, or fail mid-processing.

Important

Reference-work disclosure: this is synthetic portfolio/reference work. It is not client work, a production incident, an official Stripe sample, or a complete Stripe emulator. Every ID, event, amount, secret, and account in the tests is invented. It never contacts Stripe or processes a real payment.

Billing Webhook + Entitlement Rescue event-flow preview

Verified outcome

Failure mode Explicit behavior Executable evidence
Forged or stale signature Reject before parsing or state mutation mutations=0
Duplicate event ID Return the prior disposition without applying twice deduplicated=true
Older lifecycle snapshot Keep the higher authoritative version stale_lifecycle_ignored=true
Older invoice outcome Keep the newer state for that invoice stale_invoice_ignored=true
Payment failure then recovery Revoke, then re-grant from server-side rules Tested transition
Refund Record separately; never imitate cancellation refund_separate=true
Subscription deletion Reconcile authoritative deletion and revoke deleted_revoked=true
Processing failure Retain a visible pending dead letter for explicit replay explicit_replay=true

Run the evidence with Node.js 20 or newer:

npm ci
npm run check
npm test

Expected evidence markers:

SIGNATURE GUARD VERIFIED forged_rejected=true stale_rejected=true mutations=0
ENTITLEMENT RESCUE VERIFIED deduplicated=true stale_lifecycle_ignored=true stale_invoice_ignored=true refund_separate=true deleted_revoked=true
DEAD LETTER VISIBILITY VERIFIED resolved=1 pending=2 explicit_replay=true
tests 3 | pass 3 | fail 0

No Docker, webhook tunnel, Stripe account, environment values, hosted database, or real credentials are required.

Processing model

flowchart TD
    A["Raw delivery"] --> B{"HMAC-style signature and timestamp valid?"}
    B -->|No| C["Reject; record body digest only"]
    B -->|Yes| D{"JSON event valid?"}
    D -->|No| E["Pending dead letter"]
    D -->|Yes| F{"Event ID seen?"}
    F -->|Yes| G["Return prior disposition"]
    F -->|No| H{"Event family"}
    H -->|Subscription updated or deleted| I["Fetch authoritative subscription"]
    I --> J{"Snapshot version newer?"}
    J -->|No| K["Ignore stale lifecycle snapshot"]
    J -->|Yes| L["Replace lifecycle state"]
    H -->|Invoice paid or failed| M{"Newer outcome for invoice?"}
    M -->|No| N["Ignore stale invoice outcome"]
    M -->|Yes| O["Update invoice ledger"]
    H -->|Charge refunded| P["Update separate refund ledger"]
    I -->|Lookup or validation fails| E
    H -->|Unsupported| E
    E --> Q["Explicit operator replay"]
    Q --> H
    L --> R["Derive entitlement server-side"]
    O --> R
    P --> R
Loading

The webhook is a notification, not the lifecycle source of truth. Subscription events trigger the injected AuthoritativeBillingReader, and a monotonic snapshot version prevents a stale read from rolling state backward. Invoice outcomes are ordered independently per invoice. Refund records are a separate accounting concern and never mutate subscription lifecycle.

The entitlement decision in entitlement.ts is computed from server-held state:

  • an active trial grants access until its reconciled period end;
  • an active subscription requires the latest applicable invoice to be paid;
  • a later payment failure revokes access and a later paid event can restore it;
  • past-due, canceled, deleted, or expired lifecycle state denies access;
  • refund amount is deliberately absent from the access decision.

These rules are illustrative product policy, not universal Stripe guidance.

Evidence map

Scope boundaries

This repository models one customer with one relevant subscription and an in-memory state store. It does not implement Stripe's API schema, SDK, webhook endpoint framework, persistence, queue, Connect account context, invoice line items, proration, disputes, tax, multi-currency accounting, authorization, or operational alerting. The signature format is Stripe-like for teaching purposes but is not offered as a drop-in replacement for Stripe's official library.

For production adaptation, use Stripe's maintained server SDK to verify the raw request body, a transactional database with a unique event-ID constraint, an authorized server-side retrieval path, encrypted secret storage, and monitored queue/dead-letter infrastructure. Read the residual risks before reusing any design.

Is this pattern relevant to your blocker?

This reference is closest to a subscription application whose access has drifted from billing state, whose webhooks apply twice or arrive out of order, or whose failed deliveries disappear without an operator recovery path. Open a public redacted fit check with synthetic facts only.

GitHub issues are public. Never include API keys, webhook secrets, customer or payment data, private URLs, proprietary code, or production event payloads. Security-sensitive reports belong in the private flow described in SECURITY.md.

License

MIT

Releases

Packages

Used by

Contributors

Languages