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](/docs/building-features/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](/docs/billing-entitlements/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](/docs/billing-entitlements/gating-a-feature) for how to wire that into a new
resource, and [Authorization](/docs/core-concepts/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](/docs/billing-entitlements/gating-a-feature).
