1. Define features and plans
import { BaseSDK, featureFlag, plan, quotaFeature, subscriptionProduct } from "@effect-x/purchase"
const premiumAccess = featureFlag({ id: "premium_access" })
const apiCalls = quotaFeature({ id: "api_calls" })
const plans = [
plan({
id: "free",
group: "main",
default: true,
includes: []
}),
plan({
id: "pro_monthly",
group: "main",
price: { amount: 12, interval: "month" },
includes: [premiumAccess(), apiCalls({ limit: 100_000, reset: "month" })],
provider: {
stripe: "pro_monthly",
paddle: "pro_monthly"
}
})
] as const
const products = [
subscriptionProduct("app", {
name: "App",
plans
})
] as const
export class Pay extends BaseSDK<Pay, Record<string, never>, typeof plans, typeof products>({
plans,
products
}) {}This is the core modeling idea:
- features describe benefits
- plans package those benefits into a sellable option
- products group related plans into a commercial family
- Purchase turns that into a normalized catalog the workflows can use
2. Provide a payment implementation
import { Stripe } from "@effect-x/purchase"
import * as Layer from "effect/Layer"
export const PayLive = Pay.layer(Pay).pipe(Layer.provide(Stripe.layer))You can swap Stripe.layer for Paddle.layer, or choose the provider dynamically through PayProvider.
The example app in this repository uses the same shape with a richer catalog:
export class Pay extends BaseSDK<Pay, {}, typeof CommercialPlans, typeof CommercialProducts>({
plans: CommercialPlans,
products: CommercialProducts
}) {
static Layer = Pay.layer(Pay)
static Stripe = Pay.Layer.pipe(Layer.provide(Stripe.layer))
static Paddle = Pay.Layer.pipe(Layer.provide(Paddle.layer))
}3. Start checkout
const result =
yield *
sdk.checkout.start({
customerId,
offerId: "app:pro_monthly",
successUrl: "https://app.example.com/billing/success",
cancelUrl: "https://app.example.com/billing/cancel"
})The result gives you a provider-neutral checkout intent plus the provider checkout session reference.
Typical fields in the checkout result are:
intentIdprovidertarget.productIdtarget.offerIdcheckoutSessionIdcheckoutUrl
4. Handle webhooks
Your application receives the raw provider payload and passes it into the shared webhook runtime:
const result =
yield *
sdk.webhooks.handle({
provider: "stripe",
signature: request.headers.get("stripe-signature") ?? "",
body: await request.text()
})Purchase normalizes provider events, updates workflow state, and refreshes customer projections.
The webhook result is deliberately operational. It includes:
acceptedproviderEventIdnormalizedEventsreconciliationTriggers
5. Read customer state
const snapshot = yield * sdk.customer.getSnapshot({ customerId })
const entitlements = yield * sdk.customer.getEntitlements({ customerId })This is the main usage pattern: workflows write commercial events and state, and your app reads normalized customer state back out.
Recommended application flow
- Resolve or create your app customer
- Start checkout from a stable commercial
offerId - Redirect to the provider checkout URL
- Let webhooks finalize durable state
- Read
getSnapshotandgetEntitlements - Gate product behavior from the normalized result