Skip to Content
PaymentsSuperwall

Superwall

Use Superwall  campaigns to control the onboarding and quota paywalls, while RevenueCat keeps handling purchases.

Prerequisites

Complete RevenueCat setup and run a development build. The app uses RevenueCat entitlements to decide Pro access.

How it fits with RevenueCat

RevenueCat paywallsSuperwall placements
Paywall designRevenueCat dashboardSuperwall dashboard
Onboarding and quota triggersIncludedIncluded
Purchases and restoresRevenueCatRevenueCat, through the app’s purchase controller
Pro accessRevenueCat entitlementRevenueCat entitlement

The boilerplate mounts Superwall when the key for the current platform, EXPO_PUBLIC_SUPERWALL_API_KEY_APPLE or EXPO_PUBLIC_SUPERWALL_API_KEY_GOOGLE, is set. A platform without a key keeps the RevenueCat paywall.

src/provider/PaywallProvider.tsx already connects Superwall’s purchase controller to RevenueCat, and src/services/paywall/SubscriptionSync.tsx keeps Superwall’s subscription status in line with the user’s RevenueCat entitlements. You configure accounts, not code.

Do not add the purchase controller or subscription sync from Superwall’s RevenueCat integration guide. Both are already mounted in src/provider/PaywallProvider.tsx. Do not enable RevenueCat observer mode either.

Placements

presentPaywall from @/services/paywall passes these names to Superwall:

PlacementTrigger
onboarding_endThe user finishes or skips the final onboarding step.
quota_exceededAn AI request needs the upgrade flow, such as an exhausted free quota.

Without Superwall, onboarding opens RevenueCat’s paywall and the quota placement opens an upgrade sheet. When Superwall is enabled, both use its registerPlacement call.

The Profile screen’s Paywall component uses RevenueCat directly. Change that screen separately if you want Superwall there too.

Setup

Superwall’s CLI creates the account resources, so your coding agent can run these steps once you have logged in. Pass --json --no-interactive to each command so it prints JSON and never stops at a prompt.

Install the CLI and log in

npm install --global superwall superwall login

superwall login opens a browser, so you run it yourself. Check it with superwall whoami --json. If you belong to more than one organization, pick one with superwall orgs use <id>.

Create the apps

Use the bundle identifier and package name from config.js. The first command creates a project around the app; the second adds Android to it.

superwall apps create "<app name>" --platform ios --bundle <ios-bundle-id> --json --no-interactive superwall apps create "<app name>" --platform android --bundle <android-package> --project <project-id> --json --no-interactive

Each command prints the project with its apps. Keep the project id, each app id and each app’s public_api_key.

Create the entitlement

The identifier must equal purchases.proEntitlementId in config.js, which is pro unless you changed it.

superwall entitlements create pro --project <project-id> --json --no-interactive

Superwall treats a user as subscribed only when their active RevenueCat entitlement ids match the entitlements on its products.

Add the products

Add every product you attached to that entitlement in RevenueCat, with the same store product id:

superwall products create <store-product-id> --name "Pro Monthly" --entitlement pro \ --project <project-id> --json --no-interactive

The command has no platform or base-plan option. Run superwall bootstrap --json and check that each product appears under its app. Fix any that do not, including an Android product’s base plan and offer, on the Products page of the Superwall dashboard.

Create the campaigns

Campaigns belong to one app, so run both commands for each app:

superwall campaigns create "Onboarding" onboarding_end --project <project-id> --app <app-id> --json --no-interactive superwall campaigns create "Quota exceeded" quota_exceeded --project <project-id> --app <app-id> --json --no-interactive

The placement names must match the code exactly.

Attach a paywall

A campaign the CLI creates holds out its whole audience and shows no paywall. Design a paywall in the Superwall dashboard, or with the Superwall MCP server, and attach it to each campaign’s audience. The CLI cannot design paywalls.

Add the keys

Add a platform’s key only once both of its campaigns have a paywall. The key hands every placement on that platform to Superwall, so a campaign that still holds everyone out shows nothing, and the RevenueCat paywall no longer runs in its place. Until then, leaving the key unset keeps the RevenueCat paywall working.

.env.local
EXPO_PUBLIC_SUPERWALL_API_KEY_APPLE="<ios public_api_key>" EXPO_PUBLIC_SUPERWALL_API_KEY_GOOGLE="<android public_api_key>"

For cloud builds, set the same values in each EAS environment your build profiles use:

eas env:set --name EXPO_PUBLIC_SUPERWALL_API_KEY_APPLE --value <key> \ --environment development --environment preview --environment production \ --visibility plaintext

Restart the development server after editing .env.local. The expo-superwall native module is already in the development build.

Verify

Run yarn skills:doctor. With the CLI logged in, it matches each key to its app, checks the entitlement, and marks a placement ❌ when its campaign holds everyone out.

Complete onboarding and trigger the quota placement. Confirm the intended paywall appears for each campaign.

Complete a sandbox purchase and a restore. Check the customer’s RevenueCat record, the Pro status in Profile and a paid AI action. A displayed paywall alone does not verify purchase handling.

After release, a paywall_skipped event with reason: "holdout" in your analytics means a placement reached a campaign that showed nothing.

Commands to leave alone

The CLI also sets up Superwall its own way, which this app does not use.

CommandWhy not
superwall integrateInstalls the SDK Superwall’s way, over the RevenueCat purchase controller this app already has.
superwall migrateMoves purchases off RevenueCat.
superwall review --fixApplies fixes measured against Superwall’s own setup. superwall review without --fix only writes a plan and is safe.
superwall ascUploads your App Store Connect .p8 key to Superwall’s proxy. The app’s submission setup keeps that key on your machine.

superwall login also installs Superwall’s agent skills. Use them for account resources only; their SDK instructions assume a different purchase setup.

Set up in the dashboard instead

Everything the CLI does is also in the Superwall dashboard : create an iOS and an Android app, the pro entitlement, the products and the two campaigns, attach a paywall to each campaign, then copy each app’s public API key into .env.local and your EAS environments.

Turning it off

Remove both EXPO_PUBLIC_SUPERWALL_API_KEY_* variables and restart the development server. For standalone builds, remove them from the build environment and rebuild.

The provider then skips Superwall, and the placement service uses RevenueCat’s paywall and the quota sheet.

How it works

PaywallProvider wraps Superwall in CustomPurchaseControllerProvider. The controller in src/services/paywall/purchaseController.ts buys and restores through RevenueCat, then reports the result to Superwall as purchased, restored, cancelled, pending or failed. It reports failure until RevenueCat is configured and holds customer information for the signed-in user, so a purchase never lands on the previous account.

src/services/paywall/SubscriptionSync.tsx identifies the Supabase user in Superwall and resets that identity on sign-out. It sets Superwall’s subscription status from RevenueCat: unknown while the user’s customer information loads, then active with the entitlement IDs, or inactive. After a purchase or restore, the controller waits for this sync before reporting success, so Superwall and RevenueCat agree when the paywall closes. The edge functions still check the entitlement with RevenueCat under the same Supabase user ID.

Last updated on