Skip to main content
This page documents how checkout completion and related billing side effects are wired from Stripe events through the queue-backed webhook consumer (BullMQ by default, SQS or another adapter per tools.queue config) to Postgres and Redis, including one-off purchase finalization and queue ack/retry behavior.

How it works

Stripe sends signed events to the backend; valid handled events are enqueued and processed once per event.id using ProcessedWebhooksEvent plus a short-lived Redis key. Handlers receive validated payload shapes (see webhookValidators.ts), not raw Stripe types.

Event-level vs business-level idempotency

One-off checkout matrix (high level)

Invoice document lifecycle ownership

Invoice table lifecycle sync is owned by Stripe invoice lifecycle events: One-off receipts are still owned by payment_intent.succeeded and processor confirmPayment finalize logic, not by invoice.created / invoice.updated.

Recurring / subscription upgrades

For non–one-off checkout, payment_intent.succeeded may run before the invoice shape is complete; the handler can return true early when invoice is missing and rely on later events. Checkout validation compares Stripe subscription line items to resolved Stripe price ids from price.ts (getUsageSubscriptionPriceIds, getStripePriceIdForProduct) — not Product.external_id. Successful upgrades hydrate subscriptions.price_id from the checked-out product’s active default price row. Flow ownership is explicit:

Queue semantics: return value and retries

Processing runs in stripeWebhookConsumer (apps/backend/src/tools/queue/consumers/stripeWebhookConsumer/index.ts): Payload validation failures: handler not run; cache key removed; transaction rolled back.

Extending this system

  1. Add the event to HandledStripeWebhookEvent and the HTTP allowlist in apps/backend/src/routers/api/stripe.ts.
  2. Add payload type in webhookTypes.ts and Zod schema in webhookValidators.ts.
  3. Implement handler(payload, eventId, transaction, tools) returning boolean.
  4. Register in index.ts getHandler switch.
  5. Add __tests__ spec and a row in the scenario matrix below when behavior is non-obvious.
  6. Run make test module=backend.

What not to do

  • ❌ Bypass signature verification — always constructEvent with raw body and webhookSigningSecret.
  • ❌ Add high-cardinality tags to metrics/logs for Stripe ids in tags — use log fields where needed (see .cursor/rules/security.mdc).
  • ❌ Depend on invoice preview Redis TTL alone for freshness — invalidation is explicit; TTL is a long backstop (organizationInvoicePreview.ts).

File conventions

What’s next?