PurchaseDocumentation
Purchase

Quickstart

Define a catalog, wire a provider, and launch checkout.

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:

  • intentId
  • provider
  • target.productId
  • target.offerId
  • checkoutSessionId
  • checkoutUrl

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:

  • accepted
  • providerEventId
  • normalizedEvents
  • reconciliationTriggers

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.

  1. Resolve or create your app customer
  2. Start checkout from a stable commercial offerId
  3. Redirect to the provider checkout URL
  4. Let webhooks finalize durable state
  5. Read getSnapshot and getEntitlements
  6. Gate product behavior from the normalized result

On this page