## The endpoints exist already

```bash
bin/rails routes | grep webhooks
```

`config/routes.rb` declares the App Store and Play notification endpoints alongside the Stripe one.
Register the App Store URL in App Store Connect and the Play one in the Play Console, or they are
routes nothing ever calls.

## What arrives

Renewals, cancellations, billing retries, refunds and grace periods, each as a notification type.
They are verified through `app/adapters/app_store.rb` and `app/adapters/play_store.rb` for the same
reason the Stripe webhook verifies its signature: the endpoint is public and unauthenticated by
necessity.

The handler writes to the same `Subscription` row a Stripe cancellation would, so entitlement logic
is unchanged.

## Cancellation is not immediate

Both stores let the subscriber keep access until the end of the period they already paid for, then
stop renewing. So the correct behavior is to keep entitlement until the expiry date in the
notification, not to revoke on receipt.

Revoking immediately is the wrong reading and it produces angry support email from people who paid
for the month.

## Refunds are different

A store-issued refund does mean immediate revocation. Both platforms send a distinct notification for
it, and treating it like a cancellation leaves someone with free access to a month they were paid
back for.

## Check for the silent version of this bug

```bash
bin/kamal app exec --interactive --reuse 'bin/rails console'
```

```ruby
Subscription.where(status: "active").where("updated_at < ?", 90.days.ago).count
```

Active subscriptions that no notification has touched in three months are either genuinely long-lived
or evidence that no notification is arriving at all. On a monthly plan, the second one is the answer.

## Test it before launch

Both platforms have sandbox environments where subscription periods are compressed to minutes, so a
full renew and cancel cycle takes an afternoon rather than two months. That is the only way to see
these notifications before real customers generate them.
