Skip to content

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

SuperwallXizor
Placement (registerPlacement)Placement (usePaywall(placement).present() / presentPaywall)
Campaign + audience filtersPlacement rules over the presentation context (dashboard → Placements → Rules, or MCP set_placement_rules)
params on registerPlacementcontext 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 attributesSubscriber attributes (set via secret-key API) + presentation context
Promotional offers / discountsofferId 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
WebhooksBuilt in: apple.* events, HMAC-signed, with retries
A/B experimentsNot 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 (keep apple.notification.raw temporarily 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

  1. Fresh install → onboarding paywall appears, purchase with a sandbox account → entitlement active in the dashboard.
  2. present({ context: { wasReferred: true } }) → the referral paywall (rule) shows; without context → the default.
  3. Buy through a slot with offerId → the discounted intro price shows in the payment sheet.
  4. Login → your backend calls /v1/subscribers/alias → the subscriber shows your user id in the dashboard.
  5. Existing TestFlight build with an active sandbox sub → first launch with Xizor → entitlement appears without a purchase (bootstrap).
  6. Trigger a sandbox refund request → apple.consumption_request webhook 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.