Billing orchestration
The multi-provider checkout drivers, provider-to-domain-event parsers, and dual-layer webhook idempotency composed on top of the open BillingProvider port.
@caisson/billing-orchestration is the commercial companion to @caisson/billing: the open package
owns raw-body signature verification, the BillingProvider port, and the DomainBillingEvent
contract; this package composes those primitives into the full verify → parse → checkout →
idempotency flow for Stripe, Paddle, LemonSqueezy, and Polar.
What it does
- Four checkout drivers:
createStripeBilling,createPaddleBilling,createLemonSqueezyBilling,createPolarBilling, each returning aBillingProvider:verifyAndParse(signature check + envelope validation + domain mapping) andcreateCheckout(REST checkout/transaction creation). - Provider → domain event parsers:
parseStripeEvent,parsePaddleEvent,parseLemonSqueezyEvent,parsePolarEventmap each provider's native webhook shape onto the oneDomainBillingEventunion the rest of the platform reads. - Dual-layer webhook idempotency:
processEventclaims a whole event exactly once;withIdempotentSideEffectclaims one named side-effect of an event, so a redelivered webhook never double-fires a non-DB effect (a Discord role push, a receipt email) even though the credit ledger is already idempotent on its own.
Install
bun add @caisson/billing-orchestrationQuickstart
import { createPaddleBilling } from "@caisson/billing-orchestration";
const billing = createPaddleBilling({
apiKey: process.env.PADDLE_API_KEY!,
webhookSecret: process.env.PADDLE_WEBHOOK_SECRET!,
env: "production",
});
// Route handler: verify the RAW body, get back a typed domain event or null.
const event = billing.verifyAndParse(rawBody, req.headers["paddle-signature"]);
const { url } = await billing.createCheckout({
accountId,
priceId,
mode: "subscription",
successUrl,
cancelUrl,
});LemonSqueezy and Polar drivers are dormant until you supply credentials, createLemonSqueezyBilling
and createPolarBilling throw a ConfigError at construction time if apiKey/storeId or
accessToken/webhookSecret are missing, so a misconfigured driver never gets constructed silently.
Idempotent webhook fulfillment
The credit ledger is already idempotent on its own insert. processEvent adds the outer claim for
everything else a webhook triggers:
import {
processEvent,
withIdempotentSideEffect,
} from "@caisson/billing-orchestration";
await withTenant(tx, accountId, async () => {
const { alreadyProcessed } = await processEvent(
tx,
event.sourceEventId,
async () => {
await grantCredits(tx, accountId, event.amountTotal);
},
);
if (!alreadyProcessed) {
await withIdempotentSideEffect(
tx,
event.sourceEventId,
"discord-role",
async () => {
await pushDiscordRole(accountId);
},
);
}
});Both run inside the caller's withTenant transaction, so a claim only persists once the work it
guards has durably committed, a mid-transaction throw rolls back the claim too, and the next
delivery retries cleanly.
Configuration
| Driver | Required config |
|---|---|
createStripeBilling | apiKey, webhookSecret |
createPaddleBilling | apiKey, webhookSecret; optional env (sandbox | production, default production), onWarn |
createLemonSqueezyBilling | apiKey, webhookSecret, storeId |
createPolarBilling | accessToken, webhookSecret; optional env (sandbox | production, default production) |
PROCESSED_EVENT_SCHEMA_SQL ships the idempotency claim table as a checksum-pinned migration
string, apply it as a new platform migration file, the same convention every other kernel-owned
schema string follows.
Composing with the base
Every driver satisfies the identical open BillingProvider port from @caisson/billing: swapping
providers is a new driver, not a rewrite of call sites. The idempotency claim table is tenant-owned:
processEvent/withIdempotentSideEffect bind account_id from the @caisson/tenancy-rls tenant
GUC, so a claim attempted outside withTenant fails a non-blank CHECK instead of landing under a
shared tenant.
Sold standalone as a single SKU; the raw-body signature verifiers and the DomainBillingEvent
contract it builds on stay open in @caisson/billing.
Org controls
WorkOS SSO sign-in, a Clerk session-verification driver, owner-gated multi-user membership, and the cross-tenant admin-write RLS layer the operator control plane mutates through.
UI Pro
The commercial component tier on the open @caisson/ui token floor, an advanced data grid, virtualized tree, ops matrix, audit timeline, and more.