Migrate from Superwall
Concept mapping (placements, campaigns → rules, identify → alias), automatic subscriber migration via the transaction bootstrap, referral discounts via offer ids, and the cutover checklist.
Xizor covers the Superwall feature set most subscription apps actually use — placements, remote paywalls, context-based routing, discounts, analytics — with two significant differences: paywalls are authored as open web code (a self-contained HTML document you or an AI agent read and write directly, served to a WebView on device — no proprietary editor lock-in), and your subscription backend (receipt verification, entitlements, webhooks) is included instead of being a separate product.
This guide maps every Superwall concept to its Xizor equivalent and walks a real migration end to end. Most apps finish in under a day; your existing subscribers migrate themselves automatically on first launch.
Concept mapping
| Superwall | Xizor |
|---|---|
Placement (registerPlacement) | Placement (usePaywall(placement).present() / presentPaywall) |
| Campaign + audience filters | Placement rules over the presentation context (dashboard → Placements → Rules, or MCP set_placement_rules) |
params on registerPlacement | context on present({ context }) — drives rules and {{context.<key>}} variables in the paywall |
| Paywall editor (closed WebView editor) | Paywall as open web code — dashboard editor or agent/MCP; publish = live instantly |
feature: callback (gated feature) | present() resolves with the purchase result; check entitlements |
| Hard paywall (no dismiss) | settings.dismissable: false on the paywall document |
Superwall.shared.identify(userId:) | POST /v1/subscribers/alias from your backend (secret key) — see below |
| User attributes | Subscriber attributes (set via secret-key API) + presentation context |
| Promotional offers / discounts | offerId on a paywall product slot — signature is created server-side automatically |
| Entitlements (via RevenueCat or custom) | Built in: useEntitlements() / Xizor.shared.entitlements() — verified server-side |
| Webhooks | Built in: apple.* events, HMAC-signed, with retries |
| A/B experiments | Not yet — use rules for deterministic routing; experiments are on the roadmap |
1. Install and configure
Follow the Expo quickstart or iOS quickstart. Replace the Superwall provider with the Xizor one:
// Before
<SuperwallProvider apiKeys={{ ios: "pk_…" }}>
// After
<XizorProvider apiKey="xz_pub_…">
On the native side, Xizor.configure(apiKey:) replaces Superwall.configure(apiKey:).
2. Recreate your placements
For every registerPlacement call, create a placement with the same identifier in the dashboard (or let your agent do it via MCP: create_placement). Example from a real onboarding flow:
// Before (Superwall)
registerPlacement({
placement: "post_signup",
params: { wasReferred, context: "onboarding" },
feature: () => safeNextStep(),
});
// After (Xizor)
const paywall = usePaywall("post_signup");
await paywall.present({
context: { wasReferred, source: "onboarding" },
onPurchaseComplete: () => safeNextStep(),
onDismiss: () => safeNextStep(),
});
3. Audience filters → placement rules
Where Superwall campaigns filtered on params, give the placement an ordered rule list. First match wins; the placement's assigned paywall is the fallback — a rule can never leave a hard gate empty (unpublished or deleted rule targets fall back too).
Example: referred users get the referral-discount paywall on post_signup:
[
{
"if": { "param": "wasReferred", "op": "eq", "value": true },
"paywall_id": "<referral-paywall-id>"
}
]
Ops: eq, neq (also matches when the param is absent), in (array of values), exists. Edit rules in the dashboard (Placements → Rules) or via the MCP tool set_placement_rules. The context is also available inside the paywall as {{context.<key>}} text variables.
4. Referral / promo discounts → offerId
If you signed promotional offers yourself (e.g. a generate-offer-signature edge function), delete that path entirely. Create the offer in App Store Connect (subscription → Promotional Offers), then set the offer id on the paywall's product slot (editor → Plans → Offer ID, or in the document):
{
"slot": "yearly",
"storeProductId": "com.app.pro.yearly",
"offerId": "referral_yearly"
}
The SDK fetches the ES256 signature from POST /v1/offers/signature at purchase time and applies the discount. A signature failure aborts the purchase — the user is never silently charged full price. Requires your Apple key in App settings → Apple (key id + .p8).
5. Identity: identify() → backend alias
Superwall's client-side identify(userId:) has no Xizor equivalent by design: the publishable key ships in your binary, and letting it claim arbitrary user ids would make your subscriber base enumerable. Instead the SDK is anonymous (an unguessable server-issued app_account_token), and your backend binds the identity after login:
// In your app: get the token and send it with your login/session call
const token = await client.getAppAccountToken(); // Swift: await Xizor.shared.appAccountToken
// On your backend (secret key):
await fetch("https://api.xizor.dev/v1/subscribers/alias", {
method: "POST",
headers: {
Authorization: `Bearer ${XIZOR_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ app_account_token: token, app_user_id: user.id }),
});
The call is idempotent. If the app_user_id already belongs to another subscriber (reinstall → fresh anonymous token), the histories are merged automatically — transactions, subscription states, events, attributes — and the duplicate is deleted. 409 conflict means the token is already bound to a different user.
6. Existing subscribers migrate themselves
No import, no receipt files. On first launch with the Xizor SDK, the cold-start bootstrap reads the store's current entitlements (Transaction.currentEntitlements — silent, no sign-in prompt) and syncs every signed transaction to the backend, which verifies the JWS against Apple's root certificates and computes entitlements. Your whole active subscriber base is live in Xizor after one app update, without users doing anything.
Then point App Store Server Notifications at Xizor (App Store Connect → App Information → App Store Server Notifications):
https://api.xizor.dev/v1/apple/notifications/<your-app-id>
Renewals, refunds, and cancellations flow in from that moment — including for users who never open the updated app.
Keeping your old pipeline during the cutover
Apple allows one production notification URL. If your old system still needs the raw notifications during migration, add a webhook endpoint in Xizor that explicitly subscribes to the apple.notification.raw event: it delivers the verbatim signed JWS (plus routing metadata) to your endpoint, so your existing consumer keeps working while Xizor processes in parallel. Remove it when you're done.
7. Refund protection
Optional but recommended: enable refund protection (App settings → Apple). Xizor then answers Apple's CONSUMPTION_REQUEST refund inquiries automatically with derived usage data (elapsed period share, trial history, delivery status) and prefers declining once ≥ 80 % of the period is consumed — which measurably reduces abusive refunds. Only enable it if your app collects the user's consent to share consumption data with Apple.
8. Delete your custom backend code
After the cutover, these typically become dead code — delete them:
- Receipt/entitlement verification edge functions (e.g.
verify-entitlement) →GET /v1/entitlements/ SDK entitlement state. - Offer signature functions (e.g.
generate-offer-signature) →offerId+POST /v1/offers/signature. - Apple notification handlers (e.g.
apple-server-notifications) → the Xizor notification endpoint + webhooks (keepapple.notification.rawtemporarily if something still depends on the feed). - Client-side StoreKit glue that finished transactions or cached entitlements — the SDK owns that now (including salvage of StoreKit's occasional false "cancelled" results).
9. Verify
- Fresh install → onboarding paywall appears, purchase with a sandbox account → entitlement active in the dashboard.
present({ context: { wasReferred: true } })→ the referral paywall (rule) shows; without context → the default.- Buy through a slot with
offerId→ the discounted intro price shows in the payment sheet. - Login → your backend calls
/v1/subscribers/alias→ the subscriber shows your user id in the dashboard. - Existing TestFlight build with an active sandbox sub → first launch with Xizor → entitlement appears without a purchase (bootstrap).
- Trigger a sandbox refund request →
apple.consumption_requestwebhook fires (and, with refund protection on, the response is sent automatically).
Stuck on something Superwall did that you can't map? Tell us — migration gaps are our highest-priority bugs.