For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: text/markdown.
Billing
Use RevenueCat or Superwall on device for checkout UX, but grant access only after webhook-verified events on the API.
Mobile billing SDKs run on the device. That is fine for presenting paywalls and starting purchases - it is not enough to unlock paid features by itself.
Client vs server
| On device | On the API |
|---|---|
RevenueCat / Superwall public SDK keys (EXPO_PUBLIC_*) | Webhook signing secrets |
| Paywall UI and purchase sheets | Signature verification |
| Optimistic UI (“thanks for subscribing”) | Customer / entitlement updates in the database |
Webhook handlers live with the shared API (often next to web billing routes) and use @workspace/billing-mobile/server:
import { webhookHandler, provider } from "@workspace/billing-mobile/server";
export const billingRouter = new Hono().post(`/webhook/${provider}`, (c) =>
webhookHandler(c.req.raw),
);Rules when you extend handlers:
- Verify the provider signature before mutating data
- Use environment-specific secrets (sandbox vs production)
- Never grant a plan solely from a client callback or deep link
API deployment required
Production purchases need a deployed API that receives provider webhooks. See Mobile billing webhooks and the web deployment checklist.
Public SDK keys
Client SDK keys for Superwall / RevenueCat are often required in the app. Treat them like EXPO_PUBLIC_ values: anyone can extract them.
Mitigations:
- Restrict keys in the provider dashboard (bundle id / package name)
- Prefer server-side entitlement as the source of truth
- Rotate keys if they leak into a public repo or wrong environment
Entitlements in the UI
Gate screens with the billing summary / entitlements returned by your authenticated API, not only with the native SDK’s local cache.
Local SDK state is useful for snappy UX; the server must still refuse unpaid access on protected routes.
Related guides
How is this guide?
Last updated on