Subscription stacks are where indie apps die. The architecture I use, the receipt validation pitfalls, and why server-side is non-negotiable.
/ 01The bill that nobody warns you about.
Monetiza had to feel like a bank app. Bank apps don't lose your money to a botched receipt check, and consumer fintech apps don't survive when they do. I've watched two founders rebuild their billing stack in year two because year one shipped on client-only validation, and both of them lost a quarter to the rewrite.
The honest reason most indie fintech apps die isn't acquisition. It's the seventy small bugs in subscription state that compound into refund storms, one-star reviews, and a support inbox that won't close.
/ 02Why I reach for RevenueCat before I reach for StoreKit.
The Apple and Google billing APIs are not the bad part. Their state machines are. A user upgrades on iOS, refunds through the App Store web flow, then re-subscribes on Android inside the same billing cycle. That's three SDKs disagreeing about who the customer is, and your app needs one answer.
RevenueCat takes the disagreement and gives me one entitlement object. I read it on the client for the UI, I trust it on the server for access, and I get my week back.
The rule I follow: never let the client be the source of truth for what a user paid for.
/ 03Server-side receipt validation, in one paragraph.
The Flutter SDK gives me a quick boolean for the paywall UI. That boolean does not gate access. Every paywalled call in the Monetiza API checks the user's entitlement against a Postgres table that RevenueCat keeps fresh via webhook. If the webhook is late, the next API call refreshes the row from RevenueCat's REST endpoint and caches the result for sixty seconds. Two layers, one source of truth, no client trust.
/ 04The four pitfalls that bit me on Monetiza.
- Sandbox receipts look identical to production receipts until they don't. Build the sandbox-vs-prod branch into your validator on day one, not after the first refund ticket.
- Family Sharing on iOS means one purchaser and multiple entitled accounts. RevenueCat handles it. Roll-your-own does not, and you'll find out from a one-star review that mentions a spouse.
- Intro offers and grace periods are different events with different downstream consequences. Treat them as different events in your analytics from commit one, or your churn graphs lie to you for a year.
- Refunds via App Store Server Notifications arrive late. The webhook fires when the App Store decides, not when the user taps refund. Plan for a sixty-second drift and write the support message that explains it.
/ 05What I'd do again on the next fintech build.
- RevenueCat from commit one. The cost is rounding error against the engineering week it saves.
- A Postgres table mirroring entitlements, updated via webhooks, queried via a single function.
- A nightly job that diffs the local entitlement table against RevenueCat's REST. Surfaces dropped webhooks before users do.
- Breadcrumbs in Sentry on every paywall decision. When a user emails saying they paid and the app says they didn't, the trail is twelve seconds away.
Monetiza closed its first year with 4.8 stars across 8,000+ reviews and 11x the subscription revenue it started with. None of that came from the paywall design. It came from the paywall being correct.
If you're shipping a Flutter app with paid tiers and you want a second pair of eyes on the billing stack, book a call. Thirty minutes, no pitch.