Why does a paying customer still see the paywall?
The local Subscription row for that account is not active. Entitlement never calls Stripe at request time, so a successful payment that produced no webhook leaves the row untouched. Check the row first, then Stripe's webhook delivery log. The third possibility is that the charge belongs to a different account than the one they are signed into.
Look at the row
bin/kamal app exec --interactive --reuse 'bin/rails console'
account = User.find_by(email_address: "[email protected]").account
account.subscription&.attributes
entitled? is true when the status is active or trialing. Anything else, including a missing
row entirely, means the gate closes.
Then check whether the webhook arrived
Stripe's dashboard has a delivery log per endpoint showing every attempt and the response your app returned. Three outcomes and three different fixes:
- No delivery at all. The event type is not enabled on the endpoint. See Stripe Setup.
- Delivered, 400. Signature verification failed, so the handler never ran.
- Delivered, 200, row still wrong. The handler ran against a different account than you expect.
The account mismatch
This is the one that looks impossible and is common. Billing belongs to an Account, not to a
User, which is what lets a team share one subscription later. If someone paid while signed in with
one email and is now signed in with another, they have two accounts, and only one of them is paying.
User.where(email_address: ["[email protected]", "[email protected]"]).map { |u| u.account_id }
Two different ids is your answer.
Fixing it by hand
Stripe is the source of truth for whether money moved. Once you have confirmed it did, activating the local row is safe and idempotent:
Subscription.activate!(account, plan: :pro)
That is the same call the webhook makes, so running it twice changes nothing.
Stop it happening again
If the webhook was the problem, fix the endpoint rather than the row. Stripe retries failed deliveries for three days, so a corrected signing secret usually replays the backlog on its own and the next customer never sees the paywall.