# Superwall: Subscription Infrastructure for iOS, Android, and Web

Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.

## Pricing

- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.

Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.

## Scale

$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.

## Infrastructure capabilities

- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan

Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.

## Migration

Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).

## Paywall product (optional, separately billable)

One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.

## Architecture

Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.

## 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

# Using RevenueCat

> **Note:** Not using RevenueCat? No problem! Superwall works out of the box without any additional SDKs.

> **Warning:** You only need to use a `PurchaseController` if you want end-to-end control of the purchasing pipeline. The recommended way to use RevenueCat with Superwall is by putting it in observer mode.

You can integrate RevenueCat with Superwall using several approaches:

1. [**Using a purchase controller:**](#using-a-purchase-controller) Use this route if you want to maintain control over purchasing logic and code.
2. [**Using PurchasesAreCompletedBy:**](#using-purchasesarecompletedby) Here, you don't use a purchase controller and you tell RevenueCat that purchases are completed by your app using StoreKit. In this mode, RevenueCat will observe the purchases that the Superwall SDK makes. For more info [see here](https://www.revenuecat.com/docs/migrating-to-revenuecat/sdk-or-not/finishing-transactions).

## Using a purchase controller

### 1\. Create a `PurchaseController`

Create a new file called `RCPurchaseController`, then copy and paste the following:

:::ios
```swift
import SuperwallKit
import RevenueCat
import StoreKit

enum PurchasingError: LocalizedError {
  case sk2ProductNotFound

  var errorDescription: String? {
    switch self {
    case .sk2ProductNotFound:
      return "Superwall didn't pass a StoreKit 2 product to purchase. Are you sure you're not "
        + "configuring Superwall with a SuperwallOption to use StoreKit 1?"
    }
  }
}

final class RCPurchaseController: PurchaseController {
  // MARK: Sync Subscription Status
  /// Makes sure that Superwall knows the customer's entitlements by
  /// changing `Superwall.shared.entitlements`
  func syncSubscriptionStatus() {
    assert(Purchases.isConfigured, "You must configure RevenueCat before calling this method.")
    Task {

      for await customerInfo in Purchases.shared.customerInfoStream {
        // Gets called whenever new CustomerInfo is available
        let superwallEntitlements = customerInfo.entitlements.activeInCurrentEnvironment.keys.map {
          Entitlement(id: $0)
        }
        await MainActor.run { [superwallEntitlements] in
          Superwall.shared.subscriptionStatus = .active(Set(superwallEntitlements))
        }
      }
    }
  }

  // MARK: Handle Purchases
  /// Makes a purchase with RevenueCat and returns its result. This gets called when
  /// someone tries to purchase a product on one of your paywalls.
  func purchase(product: SuperwallKit.StoreProduct) async -> PurchaseResult {
    do {
      guard let sk2Product = product.sk2Product else {
        throw PurchasingError.sk2ProductNotFound
      }
      let storeProduct = RevenueCat.StoreProduct(sk2Product: sk2Product)
      let revenueCatResult = try await Purchases.shared.purchase(product: storeProduct)
      if revenueCatResult.userCancelled {
        return .cancelled
      } else {
        return .purchased
      }
    } catch let error as ErrorCode {
      if error == .paymentPendingError {
        return .pending
      } else {
        return .failed(error)
      }
    } catch {
      return .failed(error)
    }
  }

  // MARK: Handle Restores
  /// Makes a restore with RevenueCat and returns `.restored`, unless an error is thrown.
  /// This gets called when someone tries to restore purchases on one of your paywalls.
  func restorePurchases() async -> RestorationResult {
    do {
      _ = try await Purchases.shared.restorePurchases()
      return .restored
    } catch let error {
      return .failed(error)
    }
  }
}
```
:::

As discussed in [Purchases and Subscription Status](/docs/sdk/guides/advanced-configuration), this `CustomPurchaseControllerProvider` is responsible for handling the subscription-related logic using the modern hooks-based approach.

### 2\. Configure Superwall (Continued)

The example above shows the complete setup. The `CustomPurchaseControllerProvider` wraps your `SuperwallProvider` and handles all purchase and restore logic through RevenueCat.

For more advanced implementations, see the [example app](https://github.com/superwall/expo-superwall/tree/main/example).

> **Note:** **Legacy Approach**: If you're migrating from the old SDK or need the class-based purchase controller, you can use `expo-superwall/compat`. However, we recommend using the modern `CustomPurchaseControllerProvider` approach shown above.

### Removed Legacy Code Section

The following section contains the legacy class-based approach. Skip to the next section for the modern configuration.

As discussed in [Purchases and Subscription Status](/docs/sdk/guides/advanced-configuration), this `PurchaseController` is responsible for handling the subscription-related logic. Take a few moments to look through the code to understand how it does this.

### 2\. Configure Superwall

Initialize an instance of `RCPurchaseController` and pass it in to `Superwall.configure(apiKey:purchaseController)`:

:::ios
```swift
let purchaseController = RCPurchaseController()

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

### 3\. Sync the subscription status

Then, call `purchaseController.syncSubscriptionStatus()` to keep Superwall's subscription status up to date with RevenueCat.

That's it! Check out our sample app for working examples:

:::ios
* [iOS](https://github.com/superwall/Superwall-iOS/tree/master/Examples/Advanced)
:::

## Using PurchasesAreCompletedBy

If you're using RevenueCat's [PurchasesAreCompletedBy](https://www.revenuecat.com/docs/migrating-to-revenuecat/sdk-or-not/finishing-transactions), you don't need to create a purchase controller. Register your placements, present a paywall — and Superwall will take care of completing any purchase the user starts. However, there are a few things to note if you use this setup:

1. Here, you aren't using RevenueCat's [entitlements](https://www.revenuecat.com/docs/getting-started/entitlements#entitlements) as a source of truth. If your app is multiplatform, you'll need to consider how to link up pro features or purchased products for users.
2. If you require custom logic when purchases occur, then you'll want to add a purchase controller. In that case, Superwall handles purchasing flows and RevenueCat will still observe transactions to power their analytics and charts.
3. Be sure that user identifiers are set the same way across Superwall and RevenueCat.

:::ios
Example:

```swift
Superwall.configure(apiKey: "superwall_public_key")
Superwall.shared.identify(userId: user.identifier)
Purchases.configure(with:
  .builder(withAPIKey: "revcat_public_key")
  .with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit1/.storeKit2)
  .with(appUserID: user.identifier)
  .build()
)
```
:::

For more information on observer mode, visit [RevenueCat's docs](https://www.revenuecat.com/docs/migrating-to-revenuecat/sdk-or-not/finishing-transactions).