## Try it now

```bash
bin/dev
```

Sign in, open the pricing page, and buy. There is no card form, because there is no Stripe: the fake
completes the session immediately and redirects back. The account now has an active subscription and
anything behind `include RequireEntitlement` opens.

## How the switch works

`app/adapters/billing.rb` is the seam. It returns a Stripe client when the credential is present and
a deterministic local implementation when it is not:

```ruby
Billing.for(account)
```

Nothing else in the app knows which one it got. Controllers, models and specs all call the same
methods, so the code that runs against the fake is the code that runs against Stripe. The two
implementations sit side by side in `app/adapters/billing`, which is worth reading once: the fake is
short, and it tells you exactly what the real one is expected to do.

This is the pattern every external service in the kit uses, including bot checking and error
reporting, which is why a fresh clone boots with zero external accounts.

## What the fake does not simulate

Be clear about the boundary, because it matters before launch:

- **Card failures.** The fake always succeeds. Declines, 3D Secure and expired cards need Stripe test
  mode with the published test card numbers.
- **Webhook timing.** Real Stripe delivers `checkout.session.completed` asynchronously, sometimes
  after the user is already back on your site. One Shot handles that with an idempotent upsert that
  both the webhook and the checkout return call, so whichever arrives first wins, but the fake never
  exercises the race.
- **Proration and plan changes.** Stripe's billing engine does that arithmetic and the fake does not.

## Moving to real Stripe

Set three variables and nothing in your code changes:

```bash
STRIPE_SECRET_KEY=sk_test_... bin/dev
```

Use a test-mode key first. The flow is identical, and now declines and webhooks are real. See
[Stripe Setup](/docs/billing-entitlements/stripe-setup) for the webhook secret and which events to
enable.
