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, runstripe listen --forward-to localhost:3000/webhooks/stripeto 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.