Elhadi.
← All posts
7 min readAI-written · in Suhaib Elhadi's voice

What RevenueCat handles that you'd otherwise have to build

This update was drafted on a schedule by the AI I build with, from real project notes — part of the vibecoding experiment this blog documents.

So the first time I looked at RevenueCat I thought it was a wrapper. A nice SDK over StoreKit, a dashboard, some charts. Apple already gives you a purchase API for free — why am I adding a third party to the one part of the app that touches money?

I put subscriptions in tab. anyway, and about two days into building it I understood. The purchase isn't the hard part. Apple really does hand you the purchase. The hard part is every single thing that happens to a subscription after the moment somebody buys it — and almost all of it happens somewhere you aren't watching.

Let me go through what that actually means, because "it handles subscriptions" is uselessly vague and it's what everyone says.

What does Apple actually give you for free?

You get a purchase flow. That's real and it's not nothing. You define your products in App Store Connect, StoreKit shows the sheet, Apple takes the payment, handles tax, handles the card, handles the refund policy, and hands your app back a transaction saying it went through.

Everything in that paragraph is genuinely solved for you. If your product were a one-time purchase that never expires and the user only ever owns one device, you could stop reading here and not use RevenueCat at all. I mean that. A single non-consumable unlock is a weekend of work with no dependency.

Subscriptions are a different animal, because a subscription isn't an event. It's a state. And the state changes without your app being open.

Why is "did they pay" harder than it sounds?

Here's the question your app actually needs answered, on every launch, in about 200 milliseconds: is this person allowed in right now?

That sounds like one boolean. It is one boolean. But look at what feeds it.

You've probably got a monthly and an annual. Maybe a lifetime. Maybe a promo price you ran once, and a legacy tier you grandfathered because you raised prices and didn't want to punish early people. That's five product IDs already, and the app doesn't care which one — it just wants to know pro or not pro. So you write a mapping. Then you change your pricing and you write it again, and now it's in two places, and one of them is shipped inside a binary that's sitting on somebody's phone and won't update for six weeks.

That mapping — many product IDs onto one entitlement, changeable from a server instead of from a build — is honestly half the value on its own. Products are a business decision and they change constantly. Access is a code decision and it should never change. Keeping those two things apart is the kind of thing you only appreciate after you've tangled them once.

What happens after the purchase that nobody warns you about?

This is the real answer to the whole post. A subscription generates events for months, and your app is closed for nearly all of them.

It renews. It fails to renew because the card expired, and Apple puts it into a billing retry period where the person still has access even though no money moved. It gets cancelled but stays valid until the end of the paid period, which means cancelled and should be locked out are completely different facts and you need both. It gets upgraded mid-cycle with a prorated refund. It gets refunded weeks later. It gets shared with a family. It gets paused, in some regions. It comes back from the dead after a grace period.

Every one of those is a state transition you did not initiate and will not observe by asking the phone. If you build this yourself you're writing a webhook consumer for Apple's server notifications, and you're writing it defensively, because notifications can arrive out of order and can arrive twice.

Then you do it again for Google if you ever ship Android, and the model is different there, and now you own two subtly wrong implementations of the same idea.

The thing that actually bit me: verification

I want to be specific about the piece I'd have gotten wrong.

The naive version of paywalling is: the app asks StoreKit if there's a subscription, StoreKit says yes, the app unlocks. Done. It works. It also means the client decides whether the client gets in, which is not a security model, it's a suggestion. A jailbroken device or a patched binary just answers yes.

The correct version is that a server you control holds the truth, validated against Apple, and your backend refuses to serve premium data to an account that isn't entitled. Not the UI — the data. Hiding a button is decoration; the API has to say no.

RevenueCat is the server that holds that truth. That's the actual product. My app asks RevenueCat, RevenueCat asks Apple, and my backend can check the same fact independently instead of trusting whatever the phone claimed. Which matters a lot more in something like tab., where the whole thing is a shared public ledger and one user's client is asking about data that belongs to other people.

And there's a version of this failure that's much worse than being ripped off: getting it wrong in the other direction. Someone pays, the validation hiccups, and your app tells a paying customer they're not a subscriber. That's not lost revenue, that's a refund and a one-star review, and it's exactly the kind of silent failure where nothing errors and nobody tells you.

What about restore, and the person with a new phone?

Apple requires a restore path. You must have it, review will look for it, and it exists because purchases belong to an Apple ID, not to your app.

But the interesting case isn't restore. It's identity. Your user signs in — in tab.'s case with Sign in with Apple — and the question becomes: is the person who paid on the old phone the same account that just signed in on the new one? Apple's receipt knows about an Apple ID. Your database knows about a user ID. Nothing automatically joins those two facts.

So you need a stable app-user identity anchored server-side, plus a sane story for the purchase that happened before anybody logged in, because people buy first and sign up second all the time. The anonymous-then-identified handoff is a genuinely fiddly problem, and it's one of those where the bug doesn't show up in testing because in testing you always log in first, like a well-behaved person who wrote the app.

Where does the money reporting come in?

This is the part I underrated going in.

Apple's own sales reporting is delayed and it's shaped for accounting, not for answering "did the thing I shipped on Tuesday do anything." Trial-to-paid, churn, which offer converted, what a cohort did — you can get there from raw Apple data, but you're building a small data pipeline to do it, and that pipeline is a whole side project with its own bugs.

What I did instead was wire RevenueCat's webhook into the admin console I built for tab., so subscription events land in my own revenue ledger as they happen. My data, my database, my queries, but I didn't have to write the part that watches Apple.

That's the trade in a sentence. I still own the ledger. I just don't own the plumbing between Apple and the ledger, and the plumbing was never going to be my competitive advantage.

So when should you not use it?

I'd skip it for a one-time unlock with no recurring anything. Genuinely — don't add a dependency for that.

I'd think hard if you're at real scale, because their pricing scales with tracked revenue, and at some point the build-vs-buy math flips and you'll have the team to act on it. That's a good problem and it's not mine.

And I'd be clear-eyed that it's a vendor in your payment path. If they're down, your entitlement checks are degraded. The mitigation is caching entitlement state locally with a sensible grace window so a blip doesn't lock out paying users — which, notably, is a thing you'd have to handle in the DIY version too, except then you'd be the one who's down.

The other honest caveat: it does not make Apple like you. RevenueCat handles the money; App Review handles whether you exist. tab. still got rejected on rules that had nothing to do with any of this — Sign in with Apple and how you're allowed to link to subscription terms. No SDK saves you from that conversation.

The honest summary

If I were starting a subscription app tomorrow I'd use it again, and my reasoning is boring: I'm one person, and the alternative isn't "write it myself once." The alternative is maintain it myself, forever, across two stores, against APIs that change on Apple's schedule and not mine.

That's the calculation I keep making everywhere — the same one behind how I pick a backend. Not "can I build this." I can probably build most of it. It's whether owning it moves anything a user will ever notice.

Nobody has ever downloaded an app because its receipt validation was hand-rolled.

This one's auto-drafted from my notes on a schedule. If a number isn't in the notes, it doesn't show up here — I'd rather leave a blank than make something up.