# 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

# Pages & Navigation

Build multi-page paywalls, onboardings, and funnels with file-based pages and a stack router, no network between steps, no loading spinners.

Multi-page paywalls, onboarding quizzes, and funnels are built from file-based pages and a stack router. Moving between pages never touches the network. The whole flow ships together, so there's no page load, no spinner, and no screen that never arrives.

## Add pages

Every `.tsx` file in `app/` is a page; directories nest the name:

```ts
app/
├── index.tsx        "index" — every flow starts here
├── plans.tsx        "plans"
├── layout.tsx       wraps every page (the one reserved name)
└── goals/
    ├── index.tsx    "goals"
    └── setup.tsx    "goals/setup"
```

File names are lowercase-kebab, and each page default-exports a component. Components that aren't pages go in `components/`, not `app/`. A stray file there is a warning in dev and blocks a push.

> **Note:** Only the top-level `layout.tsx` is special. A nested `goals/layout.tsx` would become a page named `goals/layout`. There are no nested layouts.

## Navigate

```tsx
import { useRouter } from "superwall/navigation";

const router = useRouter();

router.push("goals/setup");                    // forward
router.push("plans", { transition: "fade" });  // with a transition
router.replace("terms");                       // swap the current page
router.back();                                 // one step back
router.canGoBack();                            // anything to go back to?
router.dismiss(2);                             // back two steps
router.dismissAll();                           // back to the first page
router.dismissTo("goals");                     // unwind to it (replaces current if not in the stack)

router.name;    // current page
router.depth;   // pages underneath (index = 0)
```

If you've used expo-router, this is the same shape, minus `navigate`/`setParams`, plus `name` and `depth`. Page names autocomplete and reject typos, thanks to the generated `superwall.d.ts`, one more reason to [commit it](/docs/framework/project-structure).

A few rules make navigation feel right:

* **Closing the paywall is `useActions().close()`**, not navigation. The stack is for moving within the flow; closing hands control back to your app. See [Actions](/docs/framework/actions).
* **Pages you navigate away from stay alive.** Going back restores a page exactly as it was left, scroll position and state included. A covered page can't be clicked or focused; `useIsFocused()` tells a page it's covered so it can pause video or timers.
* **There is no declared page order.** Any page can push any page, which is exactly what makes branching flows possible.
* **Page views are tracked for you.** Every navigation reports analytics automatically; there's nothing to instrument.

## Pass state between pages

Navigation carries no params, on purpose. Cross-page state has two homes:

**`layout.tsx`** stays mounted for the whole flow, React state or context there is visible to every page:

```tsx
export default function Layout({ children }: PropsWithChildren) {
  return <div className="shell"><Chrome />{children}</div>;
}
```

**A plain module** works even after the collecting page is gone, the quiz pattern, from the onboarding quiz example (see [Examples](/docs/framework/examples)):

```ts
// components/answers.ts
export const answers: { goal?: Goal; level?: Level } = {};
```

```tsx
const choose = (value: Goal) => {
  haptics.selection();
  answers.goal = value;
  router.push("level");
};
```

Guard every read on the destination, `answers.goal ? PLAN[answers.goal] : undefined`, so a revisited page never crashes on a missing answer.

**The URL**, for flows on the web, and on a web funnel this is not one option among three. It is the rule. Neither home above survives a reload, and an in-app browser (Instagram, TikTok) hands only the link to Safari when someone taps "open in browser". Its storage stays behind. So a surface with [web checkout](/docs/framework/web-checkout) keeps its state in the page URL: the route stack goes in automatically, and **every answer, selection and input** is kept with `useQueryState`, never `useState`:

```tsx
import { parseAsStringEnum, useQueryState } from "superwall/navigation";

const [goal, setGoal] = useQueryState("goal", parseAsStringEnum(["focus", "habit"]));

setGoal("focus");        // ?goal=focus — and the plan page reads the same hook
router.push("plan");
```

The API is [nuqs](https://nuqs.dev)'s, so the parsers read the same: `parseAsString`, `parseAsInteger`, `parseAsFloat`, `parseAsBoolean`, `parseAsStringEnum`, `parseAsArrayOf`, `createParser`, each with `.withDefault()` and `.withOptions({ history, clearOnDefault })`. Any link then resumes the flow on the same step with the same answers, after a reload, in the OS browser, or back from hosted checkout, and the browser's back button is `router.back()`.

[Web Funnels](/docs/framework/web-funnels) has the full treatment, multi choice, inputs, branching, the URL budget. Three rules keep it honest:

* **Only what a page asks for is persisted.** The framework never decides what an answer is. Keys starting with `sw_`, plus `platform` and `transport`, are reserved, the hook throws on them. The route stack is one of those reserved keys, `sw_nav` — a comma-joined list of route names (`?sw_nav=offer`), validated against the surface's routes. **It decides where a surface opens even when `queryState` is off**, so a link, a QR code or a screenshot job can enter a flow at any page; with `queryState` off it is read once and never written back, so navigation from there on is plain state.
* **Mind the URL budget.** About 2 kB is safe across every app and share sheet, keep keys short and values enumerable, and keep anything personal out of a URL.
* **The same code runs natively.** In an SDK webview there is no URL bar, so `useQueryState` is plain state shared across pages. The flow reads identically everywhere. `definePaywall({ queryState: true | false })` overrides the checkout default on web builds; a native host forces it off regardless.

## Shared chrome

Put back buttons, step counters, and the close button in `layout.tsx`, and drive them from router state so they can never drift from the stack:

```tsx
const router = useRouter();

{router.canGoBack()
  ? <button onClick={() => { haptics.light(); router.back(); }}>Back</button>
  : <span className="chrome-button" />}      /* placeholder keeps the layout stable */
<span>{router.depth + 1} of 3</span>
```

> **Note:** `depth + 1` works as a step counter only in linear flows. In a branching flow, a page's depth isn't its step number. Label steps per page instead.

The framework wraps the layout in a flex column that already fills the viewport inside the paywall's [insets](/docs/framework/styling#insets), so a layout's shell is a flex child, not a full-height box:

```css
.shell { display: flex; flex: 1 1 auto; flex-direction: column; }
```

The pages fill whatever the shell leaves them. A shell that sets `min-height: 100dvh` overflows the wrapper by the insets and scrolls; a layout that isn't a flex column can size the pages explicitly with `--sw-routes-height` on `:root` (default `auto`). `--sw-background` is needed on every paywall, layout or not; see [Styling](/docs/framework/styling#background-color).

Position overlay chrome absolutely *over* the pages rather than as a bar above them. Each page paints its own background, so a bar of its own shows as a seam during transitions. The layout is already inside the paywall's [insets](/docs/framework/styling#insets) and positions against the framework's content box, so `position: absolute; top: 4px` is below the status bar with no positioned ancestor of your own; give every page enough top padding to start below the chrome.

## A funnel is one paywall, not several

Multi-step flows (onboarding quizzes, web funnels) are **one paywall whose steps are pages**, not a chain of separate paywalls. Every step is a `router.push` in the same flow, so there's no load between steps and nothing to re-fetch. The structure is identical (`config.ts` plus `app/` pages plus `layout.tsx`), and funnels live in `superwall/funnels/<id>/` with exactly the same shape.

The web funnel example is the reference: question steps kept in the URL, a typed plan selector, then `purchase(reference)` at the end, with [web checkout](/docs/framework/web-checkout) taking payment in the same flow. Funnels usually want `transition: "shift"` in `config.ts`, see [Transitions](/docs/framework/transitions).

## Where transitions and animation fit

How pages move (the built-in transitions, custom ones, and bottom sheets) is covered in [Transitions](/docs/framework/transitions). Animation *inside* a page (Motion, CSS) is yours; moving *between* pages stays the router's job. Keeping that line means spamming navigation can never fight your component animations. And entry animations gate on presentation, never mount. See [Lifecycle & events](/docs/framework/lifecycle).

Assets for upcoming pages preload automatically while the user is on the current page, see [Assets](/docs/framework/assets).