@saacms/stripe-bridge
v0.1.9
Published
The reusable Stripe-webhook → saacms-Collection seam. Productionises the commerce-bridge spike's GO verdict (`docs/spikes/commerce-bridge.md`) as a host-mountable handler.
Readme
@saacms/stripe-bridge
The reusable Stripe-webhook → saacms-Collection seam. Productionises the
commerce-bridge spike's GO verdict (docs/spikes/commerce-bridge.md) as a
host-mountable handler.
- Zero runtime deps beyond
@saacms/core. NostripeSDK, no network. Signature verification is Web Crypto HMAC-SHA256 only. - Idempotency is ADR-0030 declarative
unique— the target collection declaresunique: [["stripeEventId"]]; a redelivered Stripeevent.idyields a runtime409, which this handler maps to a200ack (no duplicate row, no secondafterChange). The spike's host-side filter-precheck workaround is superseded and not reproduced here. - Pure — the saacms runtime is injected as
opts.fetch(the host passesapp.fetch) and the clock asopts.now, so the handler is fully in-process testable.
import { createStripeWebhookHandler } from "@saacms/stripe-bridge"
const handler = createStripeWebhookHandler({
fetch: app.fetch, // the saacms runtime
collectionSlug: "orders",
signingSecret: process.env.STRIPE_WEBHOOK_SECRET!,
mapEvent: (event) =>
event.type === "payment_intent.succeeded"
? { collection: "orders", body: buildOrder(event) }
: null, // ignored event types → 200 ack, no write
})
// Mount at POST /api/stripe/webhook in any host (CF Worker, Next, Astro…).HTTP status contract (the load-bearing idempotency invariant)
| Situation | Handler status |
|---|---|
| Signature missing/malformed/stale/mismatched | 400 (no write; Stripe must not retry) |
| Validly signed but body not JSON / no string id+type | 400 (no write) |
| mapEvent → null (unhandled event type) | 200 ack (no write) |
| Runtime create 2xx | 200 (created) |
| Runtime create 409 (ADR-0030 redelivery) | 200 ack (duplicate — idempotent success) |
| Any other runtime status (5xx / validation / …) | a 5xx (transient → Stripe retries) |
