# One Subscription Flow for Thousands of Apps

> How GoodBarber apps sell subscriptions: one shared service shows only what Apple can sell, lets the server confirm every purchase, and unlocks the same access on every screen.

- Context: GoodBarber · Platform
- Published: 2026-07-08 · Updated: 2026-09-29
- Reading time: 6 min
- Canonical: https://lanfranchistephane.dev/articles/in-app-purchase-infrastructure/

## System at a glance

1. Subscription page — Only offers Apple can sell
2. Who is buying — An account, or none yet
3. Apple payment — Apple's own sheet takes the money
4. Server check — The receipt is verified
5. Access — Unlocked on every screen

## At a glance

- Role: iOS engineer who designed the shared subscription service and built its StoreKit, server, and identity parts.
- Challenge: Let thousands of apps sell their own subscriptions, while only the GoodBarber server decides who gets access.
- Outcome: Buying, restoring, and moving a subscription to an account all go through one service, and every screen reads the same access.

## What was at stake

### Apple taking the payment is not the same as the reader getting access.

GoodBarber builds thousands of iOS apps from one engine. An app owner can sell subscriptions: a reader pays, and premium articles, videos, or pages unlock.

Apple handles the payment, but Apple does not know which content a subscription opens, or whether the reader has an account. The GoodBarber server knows. So the app must never unlock anything just because Apple says the payment went through. It waits for the server.

**My responsibility — the subscription service.** I designed the service that every screen relies on, and built its StoreKit, server, and identity parts. Teammates built the subscription page and the locked-content screens on top of it.

## One service, one question

Every screen asks the same question: which access levels does this reader have? One shared service answers it. No screen reads Apple's payment data itself, and access levels only ever come from the GoodBarber server.

<picture>
  <source media="(max-width: 720px)" srcset="https://lanfranchistephane.dev/articles/in-app-purchase-infrastructure/purchase-flow-narrow.svg">
  <img src="https://lanfranchistephane.dev/articles/in-app-purchase-infrastructure/purchase-flow.svg" width="960" height="276" loading="lazy" decoding="async" alt="One purchase in five steps: the subscription page shows only the offers Apple can sell; the app works out who is buying, a reader with an account or one without an account yet; Apple's own sheet takes the payment; the GoodBarber server verifies Apple's receipt and decides the access levels; access unlocks on every screen. Nothing unlocks before the server has answered.">
</picture>

## 1. Show only what Apple can sell

Each app owner chooses which subscriptions to sell, in what order, and writes the text of the subscription page. When the app starts, the service asks the App Store for the matching products.

A subscription the App Store does not return is left out, so the page never shows an offer that cannot be bought. The owner's order is kept: the order of the App Store's answer does not matter.

```pseudo title="Build the list of offers"
take the subscriptions the app owner chose, in the owner's order
ask the App Store for the matching products
for each subscription:
  if the App Store returned it:
    keep it
  otherwise:
    leave it out
```

The page only knows each offer's identifier. It asks the service for the App Store's translated title, price, billing period, and free-trial length, and places them into the text the owner wrote. Which content a subscription unlocks is not known on the phone at all: that link lives on the server.

## 2. Know who is buying

A reader can subscribe with an account, or without one.

- **With an account**, the app first asks the server whether this Apple ID already holds a subscription that can move to the account, for example one bought earlier without an account. If so, the app moves it instead of charging again.
- **Without an account**, nothing is needed before paying. After the payment, the server exchanges Apple's receipt (Apple's proof of purchase) for an anonymous identity and its access levels. The app saves both on the phone, so access survives a restart. On a new phone, "Restore purchase" brings the subscription back, still without an account.

When a reader who subscribed without an account logs in later, the app asks whether to transfer the subscription to that account. App owners can also require an account right after subscribing: the reader is invited to log in or sign up, and the subscription moves to the account.

## 3. Let Apple take the payment

Before handing over to Apple, the service checks that this iPhone is allowed to make payments, and refuses to start a second payment while one is still running. In apps for children, or when the reader is under age, a simple parental gate comes first. Then Apple's own sheet takes over.

<img class="device-shot" src="https://lanfranchistephane.dev/articles/in-app-purchase-infrastructure/purchase-sheet.webp" width="320" height="587" loading="lazy" decoding="async" alt="Reconstructed iPhone showing the Atlas Premium subscription page dimmed behind Apple's App Store purchase sheet: an annual subscription with a 7-day free trial, then €39.99 per year, confirmed with the side button.">

*The payment belongs to Apple. What happens before and after it belongs to the app.*

The code that listens to Apple's payment updates starts when the app launches and keeps listening for the whole session, not only while the subscription page is open.

```pseudo title="Listen to Apple's payment updates"
when Apple reports a payment update:
  if it is still processing, or waiting for a parent's approval:
    keep waiting
  if it is paid:
    tell Apple the payment is handled, then continue the purchase
  if it failed or was cancelled:
    tell Apple, and stop without showing a technical error
```

## 4. Let the server decide

After the payment, the app sends Apple's receipt to the GoodBarber server, together with the reader's identity. The server checks the receipt and decides which access levels the reader now has. The app then reloads the account, or saves the new anonymous identity. Only then does anything unlock, and only then does the subscription page close.

```pseudo title="From payment to access"
decide who will own the purchase: the account, or an anonymous identity created after paying
if the reader is signed in and this Apple ID already holds a subscription the account can take:
  move it to the account, reload the account, and stop
ask Apple to take the payment
send Apple's receipt and the reader's identity to the server
reload the account, or save the anonymous identity and its access levels
close the subscription page
```

If Apple's receipt is missing from the phone when a signed-in reader buys or anyone restores, the app asks Apple for a fresh one. Only one request goes out: anyone else who needs the receipt waits for that same answer.

## 5. Unlock the same access everywhere

When the access levels change, the whole app hears about it at once.

- **Restricted content** compares the levels it requires with the reader's. The free feed only carries a preview; the full version is fetched with the reader's identity and kept on the phone per access level. When a reader loses a level, the full versions kept for it are deleted.
- **Ads** disappear for subscribers when the app owner turned that option on.
- **The subscription page** is for people who have not subscribed. A subscriber who taps "Subscribe" lands on their profile instead, where "Manage my subscription" opens Apple's settings if the subscription was bought on an iPhone.

<img class="device-shot" src="https://lanfranchistephane.dev/articles/in-app-purchase-infrastructure/purchase-states.webp" width="1578" height="1082" loading="lazy" decoding="async" alt="Three reconstructed iPhone screens. First, the subscription page shows the offers and a Restore purchase link. Then Apple's purchase sheet covers the dimmed page. Last, the subscriber guide is open and readable.">

*Before, during, and after the payment. After a purchase, the page simply closes and the reader is back on the content, now unlocked.*

:::note
The iPhone screens are reconstructions of a fictional app, drawn for this article. They are not screenshots of a customer's app.
:::

## When things do not go to plan

| What happens | What the reader sees |
|---|---|
| The reader cancels on Apple's sheet | Nothing: no error message |
| The payment waits for a parent's approval | The request stays open; if it is approved while the app is running, the purchase carries on |
| Apple's receipt is missing | The app asks Apple for a fresh one, once |
| A second payment starts while one is running | It is refused |
| The subscription belongs to another account | Restore does not move it, and says it cannot be restored |
| The server check fails after paying | An error; "Restore purchase" brings the subscription back |

## Trade-offs

- **Apple is told the payment is handled before the server has checked it.** This keeps Apple's queue clean, but if the server check then fails, the reader has paid and sees an error. "Restore purchase" is the way back.
- **One shared service means more recovery code in one place.** In return, no screen reads Apple's payment data or makes its own access decisions.
- **Built in 2021 on the original StoreKit**, with receipts and a payment queue. Only Apple's "Manage subscriptions" sheet uses the newer API.

## Related

- [One Layout Engine for the App's Content Lists](https://lanfranchistephane.dev/articles/layout-engine/) — GoodBarber · Platform, 8 min read. How GoodBarber's list templates got one shared layout: the same code sizes and places a list on the home page and on a full screen, measures each item with the view that draws it, and slots native ads between items.
- [Asking for Age Before Asking to Track](https://lanfranchistephane.dev/articles/privacy-feature-gates/) — GoodBarber · Platform, 7 min read. How GoodBarber apps ask for a person's age range before any tracking prompt, keep people under 16 away from ad networks, third-party analytics, and location, and check that answer again each time one of them runs.
