Docs · Billing & Entitlements

How Billing Works

The Local fake versus real Stripe, and what flips a build over to live billing.

Billing follows the same adapter-plus-fake seam every other integration in the kit uses. The presence of one environment variable, STRIPE_SECRET_KEY, is what flips the whole app from a Local billing fake to real Stripe. Nothing in your controllers or views needs to know which one is active; they all talk to Billing.for(Current.account).

The Local fake

Without a Stripe key, Billing.for(account) returns a fake that behaves like Stripe closely enough to build and test against: it tracks a subscription state, honors entitlement checks, and lets you exercise the full paid-feature flow without a Stripe account, a webhook, or a network call. This is why a fresh clone of the kit runs its entire billing surface, including the pricing page and the entitlement gate, before you've configured anything. See The Adapter+Fake Pattern for the general mechanism this is built on.

Turning on real Stripe

Set STRIPE_SECRET_KEY (start with a test key, sk_test_...) and the app starts talking to Stripe for real. You'll also need:

  • STRIPE_WEBHOOK_SECRET, from your webhook endpoint at /webhooks/stripe. Locally, run stripe listen --forward-to localhost:3000/webhooks/stripe to get a matching secret.
  • A price id for each plan you offer, for example STRIPE_PRICE_PRO, created in Stripe or through the Stripe MCP server (see .mcp.json).

See Stripe Setup for the full checklist, including the statement descriptor you should set before charging anyone.

How a controller checks entitlement

Billing.for(Current.account).entitled? is the one question every paid feature asks. You rarely call it directly: RequireEntitlement, included in a controller, calls it on every action and redirects to pricing when it's false. See Gating a Feature for how to wire that into a new resource, and Authorization for how entitlement and Pundit's record-level checks fit together as two separate layers.

Webhooks

Stripe delivers subscription lifecycle events (created, updated, canceled, payment failed) to /webhooks/stripe, which verifies the signature before acting on anything. This is what keeps entitled? accurate without polling Stripe on every request: the account's local billing state updates the moment Stripe's state changes, not the next time someone happens to check.

Next

Add the entitlement gate to a resource of your own: Gating a Feature.