Skip to main content
Back to Blog
engineering

Not Difficult, Just Time-Consuming: Subscriptions Across Web, iOS, and Android

19 May 202611 min read
StripeRevenueCatSubscriptionsiOSAndroidSupabaseSolo FounderHavnwright
Share:

A Note on Expertise

I'm not writing as an "expert" or claiming to have all the answers. I'm a builder sharing my journey on what worked, what didn't, and what I learned along the way. The tech landscape changes constantly, and with AI tools now available, the traditional notion of "expertise" is evolving. Take what resonates, verify what matters to you, and forge your own path. This is simply my experience, offered in the hope it helps fellow builders.

When I started building the subscription side of Havnwright, I had one goal in mind. Use Stripe. Only Stripe. One billing provider, one dashboard, one webhook surface, one source of truth. Web and mobile would both check entitlement against the same Stripe customer record. Done.

That was not the plan I ended up shipping.

This is the post I wish someone had handed me on day one of that work. I am writing it as a record for my future self when I have to do it again, and as a public reference for anyone else who is about to discover what the Stripe-only plan does not survive contact with.

What I wanted, what I got

The setup I shipped is two payment systems sitting on top of one backend.

Stripe handles subscriptions on the Havnwright web app. Contractors who sign up through the web go through the standard Stripe checkout flow, end up with a Stripe customer record, and the subscription state syncs into my Supabase backend through webhooks.

RevenueCat handles subscriptions on the Havnwright Contractor mobile app, which is live on both iOS and Android. The mobile app uses native in-app purchase on each store. Apple takes its cut on iOS, Google takes its cut on Android, RevenueCat sits on top of both and gives me a unified API plus a single webhook surface I can integrate with Supabase.

The thing I want to be clear about is that I did not pick this architecture. I tried to avoid it. The architecture is what was left over after Apple and Google said no.

Why Apple and Google said no

The rule is the one everyone has heard about and most people underestimate until they walk into it.

Native mobile apps on iOS and Android are not allowed to send users to a third-party payment surface to subscribe to in-app features. You cannot put a "subscribe on the web" button in your app that links out to Stripe. You can put information about the subscription in your app, but the moment the app is the place the user is using the feature, the purchase has to go through the platform's billing system.

This is a hard rule. It is enforced through the app review process. If you submit an app that violates it, your app gets rejected. I know this because I tried to work around it twice.

The workaround I attempted was to remove the subscription page from the app entirely, leaving only a sign-in screen. The intent was that contractors would subscribe on the web with Stripe, then sign in on the mobile app and have everything unlocked because the entitlement was already in the backend. From an architecture point of view it was clean. The mobile app never touched billing. All money went through Stripe.

From a user experience point of view it was a disaster.

A contractor who installs the app, opens it, and sees a sign-in screen with no clear path to subscribing has been handed a brick. They have no way to know they have to go to a separate website, find the right page, create an account, subscribe, then come back. Most of them will simply not do it. The drop-off would have been brutal.

The agent I was working with at the time told me this would not survive review. I did not want to hear it. I pushed through with the workaround, submitted the app twice, and got rejected twice. Both rejections were specifically about the missing in-app subscription path. After the second rejection I accepted the obvious: there is no way to avoid native billing on mobile, and pretending otherwise is wasting submissions.

The shape of the actual setup

Once I accepted that I had to ship native subscriptions, the work was straightforward in concept and dense in practice.

You set up a subscription product on App Store Connect with its own SKU, its own pricing, its own trial period, its own renewal terms. You set up the equivalent on Google Play Console, with the same conceptual subscription but a different SKU and the same pricing translated into Google's tier system. Then you set up RevenueCat, link both store accounts to it, create entitlements that map across both stores, create offerings that group your products together for presentation in the app, and wire RevenueCat's webhooks back to your own backend so your database knows when a subscription starts, renews, lapses, or cancels.

Then, separately, on the Stripe side for the web, you have a price object that has to match conceptually with what you set up on the two stores. You have a webhook endpoint receiving Stripe events and writing the same subscription state into the same backend.

Everything has to talk to everything. The mobile app talks to the store. The store talks to RevenueCat. RevenueCat talks to your backend. Your backend resolves entitlement when the app asks. The web app talks to Stripe. Stripe talks to your backend. Your backend resolves entitlement when the web app asks. There are at least six places to check when something breaks: App Store Connect, Play Console, RevenueCat, Stripe, your webhook handlers, your database. Plus the platform sandboxes when you are testing.

Not difficult, just time-consuming

Here is the framing that helped me get through it.

None of the individual steps are hard. Configuring a subscription product on App Store Connect is not technically demanding. Setting up an entitlement in RevenueCat is straightforward once you have read the docs. Writing a webhook handler for any of these services is a half day of work each.

The reason it feels overwhelming is volume. You are doing roughly the same simple step in four or five different dashboards, with slightly different terminology in each, and the meaning of "subscription" or "product" or "entitlement" shifts subtly depending on which dashboard you are in. You are not solving a hard problem. You are doing forty easy things in a row, and any one of them being subtly wrong means the whole loop does not work and you have no idea which of the forty things is the culprit.

Different problem, different mitigation. You cannot get faster at this by being cleverer. You can only get faster by writing things down as you go, so when you have to do it again, you do not start from scratch.

The thing I would put on a poster in my office, if I had an office, is this. Time-consuming work feels difficult when you treat it as one big task. Time-consuming work becomes manageable when you treat it as a sequence of small tasks with a notebook open beside you.

What surprised me

Two surprises are worth naming because both cost me real time and neither was in any tutorial I read.

The first was the pricing display in TestFlight. I had configured my prices in App Store Connect in pounds, which is the currency for my UK-incorporated business. When I installed the build on my phone through TestFlight and opened the subscription screen, the prices showed in dollars. My first reaction was that I had something wrong in my setup, and I spent the next two hours going through every dashboard checking the currency configuration. The answer turned out to be that TestFlight, in its sandbox mode, displays prices in USD regardless of what you have configured for production. The production build behaves correctly. The sandbox behaves wrong. There is no warning about this in the dashboard. You discover it by losing two hours and then finding the right thread on the Apple developer forum.

The second surprise was Apple's fixed price tier system. On Stripe I am used to setting whatever price I want. Want to charge 3.87? Set 3.87. On App Store Connect, no. Apple has a list of allowed prices and you pick from the list. The list is curated around the conventional pricing points: 0.99, 1.99, 2.99, 3.99, 4.99, 5.99, 9.99, and so on. You can use 4.99 or 5.99. You cannot use 4.50 or 5.25. If your business model depended on a specific arbitrary price point, you have to round to the nearest allowed tier. For most businesses this is not a real problem. For a founder who has not used iOS in-app purchase before, it is something you discover at the moment you are trying to enter your intended price and the dropdown does not have it.

Neither surprise is technical in the deep sense. Both are tax-on-your-time of the form "the platform has a behaviour you cannot know about until you hit it." They are the reason a fully outsourced setup is risky for a solo founder. The person you outsource to has hit them before and will not warn you because to them they are not surprises any more.

The one part I am proud of

The entitlement layer.

A user who pays through Stripe on the web and a user who pays through native billing on mobile end up in the same place in my system. The webhook handlers from both Stripe and RevenueCat write to the same subscription record in Supabase. When either the web app or the mobile app needs to know "is this user subscribed and to what tier," they both ask the backend. The backend does not care which billing provider paid. It cares about the current state of the subscription record.

The practical implication is that a contractor who subscribes on web with Stripe can install the mobile app, sign in, and have all subscribed features available immediately. No second subscription required. No "your web subscription is for the web." The web subscription is for the user, and the user has the entitlement on every surface they sign in from. The same is true in reverse: a contractor who subscribes through the iOS app and then signs into the web dashboard sees the same entitlements.

I cannot overstate how much this matters for the user experience. The whole reason I did not want to ship the split architecture was that I assumed it would force me to ship a split experience. It did not, because the entitlement layer lives in my code and not in any single billing provider. Stripe and RevenueCat are payment surfaces. The subscription record is mine.

This is the architectural lesson buried inside all the dashboard configuration. The provider is not the source of truth. The provider is the place the money came in. The source of truth is what you write into your own database after the webhook fires.

What I would tell another founder

Three things.

Do not fully outsource this if you cannot debug it. When something breaks, and it will, you need to know which dashboard to open first. Apple, Google, RevenueCat, and Stripe all update their systems independently. They change webhook formats, deprecate endpoints, adjust their UI, and rename concepts without coordinating with each other. When the subscription state on your backend stops matching the subscription state on the provider, the bug is almost always at one of the seams between providers. The person who can find it is the person who set the seams up.

Spend the hour or two upfront to learn the topology. You do not need to be an expert on every dashboard. You need to know what each dashboard is responsible for, where its data comes from, and where its data goes. Once you have that mental model, debugging gets dramatically faster because you know which dashboard to check first when a specific symptom appears.

Write the steps down as you go. If you do this work once a quarter, you do not need to be able to do it from memory. You need to be able to do it from your own notes. The cost of writing the notes is twenty extra minutes spread across the setup. The saving when you have to do it again is hours.

The honest postscript

I have done this end-to-end exactly once. If you asked me to redo it from scratch from memory, I could probably do about a third of it without looking anything up. The rest I would have to research again because the dashboards are dense and the steps are specific enough that you do not retain them between setups unless you are doing this several times a month.

That is the unglamorous truth about cross-platform subscription work. It is not the kind of skill you build by doing it once. It is the kind of skill you build by doing it once, taking good notes, and trusting your past self to have written something useful for your future self.

Most of what I know now I learned by losing time to one of the gotchas above. The notes I have are the only reason I will not lose the same time when I have to do this for the next product.


This is part of a series about building products as a solo founder. Earlier posts cover when the agent is wrong and sounds right and why the Founder Knowledge Graph exists. More coming.

About the Author

Alireza Elahi is a solo founder building products that solve real problems. Currently working on Havnwright, Publishora, and the Founder Knowledge Graph.

Related posts