Skip to content
Paykit
Esc
↑↓navigate↵open⌘Jpreview
On this page

Stripe

Sell the catalog through Stripe Checkout, with prices and meters the deploy creates.

Stripe hosts the payment page, calculates tax, sends invoices and retries a failed payment. Paykit creates the products, prices and meter the catalog states, then records what Stripe collects.

Deploy the vendor objects

StripeBilling creates a product per sold plan, a price per price the catalog states, and a meter per metered feature. It returns the provider the billing stack validates, plus the ids the Worker needs:

import * as Stripe from "@alchemy.run/stripe";
import { StripeBilling } from "@potion.run/paykit-stripe";
import * as Layer from "effect/Layer";

const providers = Layer.mergeAll(Cloudflare.providers(), Paykit.providers()).pipe(
  Layer.provideMerge(Stripe.providers()),
);

const billing = yield* StripeBilling("StripeBilling", {
  secretKey,
  origin: "https://app.example.com",
  tiers: stripeTiers,
}).pipe(Effect.provide(Stripe.providers()));

const recorded = yield* Paykit.BillingStack("Billing", {
  catalog: billingCatalog,
  providers: [billing],
});

stripeTiers repeats the catalog’s prices in the shape Stripe sells them: { pro: { price: proPrice, usage: { messages: messagePrice } } }. The deploy copies each price onto the object it creates, so the model and the invoice agree.

billing.sold carries the ids: sold.prices.pro, sold.usage.pro.messages and sold.meters.messages. Bind them into the Worker’s environment.

billing.webhookEndpoint is the endpoint the deploy created: resource is the vendor object, and secret is the signing key Stripe minted at create. Bind the secret into the Worker’s environment, so no one copies it out of the dashboard:

const worker = yield* Cloudflare.Worker("Worker", {
  main: "./src/Worker.ts",
  env: { STRIPE_WEBHOOK_SECRET: billing.webhookEndpoint.secret },
});

Build the runtime provider

The Worker resolves the lookup keys and the meter name from its environment. A lookup key is stable, but it carries the stage, so the Worker reads the deploy’s output instead of rebuilding the convention:

import { Stripe } from "@potion.run/paykit-stripe";
import * as Redacted from "effect/Redacted";

export const provider = Stripe({
  secretKey: Redacted.make(env.STRIPE_SECRET_KEY),
  origin: "https://app.example.com",
  tiers: {
    pro: {
      price: proPrice,
      lookupKey: env.STRIPE_PRICE_PRO,
      usage: {
        messages: { unitPrice: messagePrice, lookupKey: env.STRIPE_MESSAGES_PRICE },
      },
    },
  },
  meters: { messages: env.STRIPE_MESSAGES_METER },
  webhookSecret: Redacted.make(env.STRIPE_WEBHOOK_SECRET),
});

The provider needs the secret key at runtime. The billing stack takes the same key from the same environment variable, so one value drives the deploy and the Worker.

Deliveries

StripeBilling creates the endpoint at ${origin}/webhooks/stripe — webhookPath moves it — and subscribes it to exactly the events the provider decodes, so the vendor’s event list cannot drift from what paykit handles. A delivery is verified with the secret the endpoint minted, decoded by the provider, then applied as a command. An account becomes active on the delivery, not on the redirect from checkout.

Destroying the stack deletes the endpoint: it is configuration, not financial history, and a retired deployment must stop receiving deliveries. A deployment that already owns one — a shared API, a secret in a vault — passes webhookSecret instead, and nothing is created.

client is for a test only. A deployment omits it, and Stripe builds its own client from the secret key.

What the catalog needs

A metered feature billed in arrears needs a unitPrice on the plan item, because Stripe bills the meter. A plan item without a price is counted or blocked, never sent to the meter.

See Credits if the plan grants a pool instead, and the Stripe example for the whole loop: a deploy, a checkout, a signed delivery and a test that needs no Stripe account.

The pages are Stripe, StripeBilling, and StripePrices.

Was this page helpful?