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 paywalls | Superwall placements | |
|---|---|---|
| Paywall design | RevenueCat dashboard | Superwall dashboard |
| Onboarding and quota triggers | Included | Included |
| Purchases and restores | RevenueCat | RevenueCat, through the app’s purchase controller |
| Pro access | RevenueCat entitlement | RevenueCat 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:
| Placement | Trigger |
|---|---|
onboarding_end | The user finishes or skips the final onboarding step. |
quota_exceeded | An 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 loginsuperwall 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-interactiveEach 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-interactiveSuperwall 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-interactiveThe 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-interactiveThe 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.
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 plaintextRestart 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.
| Command | Why not |
|---|---|
superwall integrate | Installs the SDK Superwall’s way, over the RevenueCat purchase controller this app already has. |
superwall migrate | Moves purchases off RevenueCat. |
superwall review --fix | Applies fixes measured against Superwall’s own setup. superwall review without --fix only writes a plan and is safe. |
superwall asc | Uploads 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.