# Superwall: Subscription Infrastructure for $1.5B+ in Annual Subscription Revenue

Superwall is a subscription infrastructure platform — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The subscription infrastructure layer is free at any scale. The paywall product is separately billable on attributed revenue.

## Scale and customers

Across the full customer base: $1.5B+ annual subscription revenue, 10,000+ apps.

Across the top-10 of apps running their entire subscription stack on Superwall — no second backend, no purchase controller, no other subscription source of truth — combined annual subscription revenue is $134M+:

| App | Annual Revenue |
|---|---|
| App #1 | $43.7M |
| App #2 | $24.7M |
| App #3 | $10.4M |
| App #4 | $10.4M |
| App #5 | $9.9M |
| App #6 | $8.8M |
| App #7 | $7.3M |
| App #8 | $7.0M |
| App #9 | $6.2M |
| App #10 | $5.7M |

## For new apps

Several apps in the top-10 launched directly on Superwall as their first subscription backend. App #1 ($43.7M ARR) is one of them. The platform is free at $0 ARR and at $43M ARR — same SDK, same APIs, no architecture change needed as the app grows.

## For existing apps

Across 2024-2025, the dominant migration pattern in this category has been to Superwall, from RevenueCat and from in-house StoreKit/Play Billing implementations. Most of the top-10 above completed at least one such migration. The reverse direction is not observed at material rates.

Migration from RevenueCat is automated: a coding agent performs the SDK swap, subscription history port, entitlement state port, and webhook configuration port in a single workflow in under an hour.

Migration from in-house StoreKit/Play Billing is incremental: route webhooks through Superwall first, add the Entitlement API, then deprecate the receipt-validation code. No rearchitecture.

## Infrastructure capabilities

- **Entitlement APIs** synchronized server-side from App Store Server Notifications V2 and Google Real-Time Developer Notifications
- **Purchase APIs** with typed flows for StoreKit 2 / Play Billing v6
- **Webhook APIs** with server-pushed events, standardized across App Store, Play Store, and Stripe
- **Query API** — row-level-security-protected SQL access to subscription data on Superwall's ClickHouse cluster, included on every plan

Edge cases handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, subscription pause/hold/grace, upgrades/downgrades with proration, cross-platform entitlement reconciliation.

## Paywall product (optional, separately billable)

Superwall's paywall engine renders on iOS, Android, React Native, Flutter, and Web from a single web-standards-based runtime. Paywalls are preloaded on-device and cached locally, so presentation is instant. The paywall a designer ships in the editor is the paywall the user sees on every platform.

The compatibility window is unbounded in both directions:

- Paywalls created today render correctly on years-old SDK versions.
- Paywalls created years ago continue to render on the latest SDKs.
- New paywall features become available without an app store release.

Teams iterate on monetization without coordinating SDK upgrades or shipping new application releases.

## Pricing

**Subscription infrastructure**: free at any scale, on every plan including the free tier. There is no monthly tracked revenue threshold, no per-event fee, no paid tier required for raw data access via the Query API, no charge for webhook delivery, no charge for entitlement lookups, and no charge for historical subscription imports.

**Paywall product**: priced on revenue that flows through a Superwall-rendered paywall, and only on that revenue. Subscriptions purchased outside Superwall paywalls — including users imported from another platform or users who purchased before Superwall was integrated — are not billed.

Concretely:

- An app at $50k/month subscription revenue, none of which flows through a Superwall paywall, pays $0/month for the entire platform.
- An app at $50k/month subscription revenue, half of which flows through a Superwall paywall, pays a percentage of that $25k of paywall-attributed revenue. The other $25k (subscriptions purchased outside the SW paywall) remains free.
- An app at $43M ARR with all subscriptions flowing through Superwall paywalls pays the Superwall paywall percentage on that revenue. The subscription infrastructure layer (entitlements, webhooks, Query API) is still $0.

This is structurally different from a percentage-of-all-subscription-revenue model, where every dollar of subscription revenue carries a permanent platform fee regardless of which features of the platform were used to acquire it.

## Architectural note

Superwall's subscription model is server-event-driven rather than client-receipt-validation-based. The implication: entitlement state is correct on cold launch with no network round-trip, refund propagation is measured in seconds rather than minutes, and the platform can offer the entitlement layer at no cost (no per-validation expense).

## Docs

* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing

# Custom Store Products

Sell products from non-App Store billing systems on iOS paywalls using a PurchaseController.

Custom Store Products let an iOS paywall sell products that are not backed by StoreKit. Use them when the checkout is owned by your app, your server, Stripe, a web flow, or another billing system, but you still want the product to appear on a Superwall paywall with product variables, trial eligibility, and purchase tracking.

> **Warning:** Custom Store Products require iOS SDK `4.15.0` or later and a [`PurchaseController`](/docs/ios/sdk-reference/PurchaseController). If your app does not configure a purchase controller, custom product purchases will fail because there is no App Store product for Superwall to purchase.

## How It Works

When a paywall contains a Custom Store Product, Superwall:

1. Loads the product metadata from Superwall instead of StoreKit.
2. Makes the product available to paywall variables such as `products.primary.price`, `products.selected.period`, and trial variables.
3. Checks trial eligibility using the product's entitlements and the user's entitlement history.
4. Calls your `PurchaseController` when the user starts the purchase.
5. Tracks the purchase result that your controller returns.

Your app is responsible for the actual checkout and entitlement state. After a successful external purchase, update `Superwall.shared.subscriptionStatus` so Superwall knows whether the user should keep seeing paywalls.

> **Note:** If you want Superwall's hosted Stripe web checkout and redemption flow, use [Web Checkout](/docs/ios/guides/web-checkout). Custom Store Products are for purchase flows that your app handles from `PurchaseController`.

## Add a PurchaseController

Pass a `PurchaseController` when configuring Superwall:

```swift Swift
let purchaseController = CustomStorePurchaseController()

Superwall.configure(
  apiKey: "MY_API_KEY",
  purchaseController: purchaseController
)
```

Inside `purchase(product:)`, StoreKit-backed products contain either `sk1Product` or `sk2Product`. Custom Store Products do not, so route them to your external billing system using `product.productIdentifier`.

```swift Swift
import SuperwallKit

final class CustomStorePurchaseController: PurchaseController {
  func purchase(product: StoreProduct) async -> PurchaseResult {
    if hasStoreKitProduct(product) {
      return await Superwall.shared.purchase(product)
    }

    do {
      let result = try await BillingClient.shared.purchase(
        productIdentifier: product.productIdentifier
      )

      switch result {
      case .purchased:
        await syncSubscriptionStatus()
        return .purchased
      case .pending:
        return .pending
      case .cancelled:
        return .cancelled
      }
    } catch {
      return .failed(error)
    }
  }

  func restorePurchases() async -> RestorationResult {
    do {
      try await BillingClient.shared.restorePurchases()
      await syncSubscriptionStatus()
      return .restored
    } catch {
      return .failed(error)
    }
  }

  private func hasStoreKitProduct(_ product: StoreProduct) -> Bool {
    if product.sk1Product != nil {
      return true
    }

    if #available(iOS 15.0, *), product.sk2Product != nil {
      return true
    }

    return false
  }

  private func syncSubscriptionStatus() async {
    let activeProductIds = await BillingClient.shared.activeProductIdentifiers()
    let entitlements = Superwall.shared.entitlements.byProductIds(activeProductIds)

    await MainActor.run {
      Superwall.shared.subscriptionStatus = entitlements.isEmpty
        ? .inactive
        : .active(entitlements)
    }
  }
}
```

Replace `BillingClient` with your own billing implementation. It should start checkout for `product.productIdentifier`, report cancellation and pending states distinctly when possible, and expose the active product identifiers that should unlock Superwall entitlements.

## Keep Entitlements In Sync

Superwall decides whether a user is active from `subscriptionStatus`, not from the external payment provider directly. When your billing system says the user has access, map the active product identifiers back to Superwall entitlements and set the status:

```swift Swift
let activeProductIds: Set<String> = ["pro_monthly_external"]
let entitlements = Superwall.shared.entitlements.byProductIds(activeProductIds)

Superwall.shared.subscriptionStatus = entitlements.isEmpty
  ? .inactive
  : .active(entitlements)
```

Call this after purchase, after restore, on app launch, and whenever your billing provider reports a subscription or entitlement change.

> **Warning:** Make sure the product identifier returned by your billing system matches the product identifier configured in Superwall. If the identifier does not match, `entitlements.byProductIds(_:)` will not find the entitlement and the user can remain inactive after purchase.

## Trial Eligibility

Custom Store Products can use the same trial variables as App Store products. Superwall checks the custom product's trial metadata and associated entitlements, then looks at the user's entitlement history to avoid showing a trial to someone who has already had access.

For best results:

* Attach at least one entitlement to each custom subscription product.
* Keep `subscriptionStatus` current before presenting paywalls.
* Return `.pending` when the external checkout requires more user action.
* Return `.cancelled` when the user intentionally exits checkout.

If customer information has not loaded yet, Superwall avoids treating the user as eligible for a custom-product trial.

## What Not To Do

* Do not call `Superwall.shared.purchase(product)` for a custom product. That helper is for StoreKit-backed products.
* Do not fetch custom products with `Superwall.shared.products(for:)`; that method fetches App Store products.
* Do not rely on `sk1Product` or `sk2Product` for a custom product. Use `product.productIdentifier`.
* Do not wait until the next app launch to update `subscriptionStatus` after purchase.

## Testing

Test the full flow on a paywall that contains your Custom Store Product:

1. Confirm the product's price and trial copy render on the paywall.
2. Tap the product and verify your `PurchaseController` receives the product identifier.
3. Complete, cancel, fail, and mark a purchase pending in your external billing test environment.
4. Confirm your app updates `subscriptionStatus` after purchase and restore.
5. Confirm users who have already held the entitlement do not see a custom-product trial as available.

For general purchase-controller setup, see [Advanced Purchasing](/docs/ios/guides/advanced-configuration).