# 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

# Transitions

Built-in page transitions, where to set them, and how to define your own with nothing but a name and CSS.

Navigation animates by default. The framework ships four built-in transitions, lets you set them at three levels, and makes custom ones a matter of naming an animation and styling four CSS phases — no registration, no JavaScript.

## Built-ins

* **`push`** — iOS-style: the new page slides in from the right while the one behind shifts back and dims. The default.
* **`slide`** — both pages travel: the new one slides in from the right as the current one slides out to the left.
* **`fade`** — a crossfade, one layer at a time.
* **`none`** — instant.

## Set them at three levels

The call site wins, then the page, then the surface:

```tsx
router.push("plans", { transition: "none" });   // one navigation
export const transition = "fade";               // one page (top of its file)
export default definePaywall({ transition: "slide" });   // whole surface
```

Going forward uses the *incoming* page's transition; going back uses the *leaving* one's — so a page always leaves the way it arrived.

## Tune the built-ins

Three CSS variables adjust timing and feel without replacing anything:

```css
:root {
  --sw-transition: 500ms;   /* duration */
  --sw-ease: cubic-bezier(0.28, 0.4, 0.08, 1);
  --sw-stack-dim: 0.925;    /* how much the page behind dims (the default) */
}
```

All motion respects `prefers-reduced-motion` automatically — with reduced motion on, the router settles instantly.

## Custom transitions

A transition is just a name plus CSS. Name it anywhere a transition goes, then style the four phases:

```tsx
export const transition = "zoom";
```

```css
@media (prefers-reduced-motion: no-preference) {
  [data-sw-transition="zoom"][data-sw-phase] {
    animation-duration: 420ms;
    animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  [data-sw-transition="zoom"][data-sw-phase="enter"]  { animation-name: zoom-enter }
  [data-sw-transition="zoom"][data-sw-phase="recede"] { animation-name: zoom-recede }
  [data-sw-transition="zoom"][data-sw-phase="leave"]  { animation-name: zoom-leave }
  [data-sw-transition="zoom"][data-sw-phase="return"] { animation-name: zoom-return }
}

@keyframes zoom-enter  { from { transform: var(--sw-from-transform, scale(0.85)); opacity: 0 } }
@keyframes zoom-recede { to   { opacity: 0; transform: scale(1.15) } }
@keyframes zoom-leave  { to   { opacity: 0; transform: scale(0.85) } }
@keyframes zoom-return { from { opacity: 0; transform: scale(1.15) } }
```

The four phases cover both directions of travel:

| Phase    | The page is…                        |
| -------- | ----------------------------------- |
| `enter`  | arriving on top                     |
| `recede` | being covered as you go forward     |
| `leave`  | dropping off the top as you go back |
| `return` | coming forward again as you go back |

### Rules for custom transitions

* **Always start `from` at `var(--sw-from-transform, <your value>)`** — and `--sw-from-filter` for filters. The router fills these with a page's live position when a navigation interrupts an animation, so a spammed button picks the page up where it stands instead of snapping.
* **Wrap in `prefers-reduced-motion: no-preference`.** With reduced motion on, the router settles instantly and your animation never runs.
* **Duration comes from your CSS.** The page stays mounted exactly as long as its animation runs; don't declare a duration anywhere else.
* **Omit phases you don't want.** They don't animate — that's how `fade` crossfades one layer at a time.

## Bottom sheets over the flow

For a modal-feeling page — a last-chance offer, say — define a `sheet` transition: the page slides up while the one behind scales back and dims. Darken the container behind it in the same motion by reusing the framework's timing variables:

```css
:root { --dim: 0.85; }

[data-sw-routes] {
  transition: background-color var(--sw-transition, 500ms)
    var(--sw-ease, cubic-bezier(0.28, 0.4, 0.08, 1));
}
[data-sw-routes]:has([data-sw-transition="sheet"][data-sw-phase]) {
  background-color: color-mix(in srgb, var(--bg) calc(var(--dim) * 100%), #000);
}
```

One `--dim` number drives both the page's `brightness()` and the backdrop, so they always match. Dismissing the sheet is `router.back()` — the page leaves the way it came. The abandonment offer example has the full recipe — see [Examples](/docs/framework/examples).

## Transitions vs. in-page animation

Animation libraries (Motion, plain CSS) animate *inside* a page. Moving *between* pages stays the router's job. Keep that line and spamming navigation can never fight your component animations — and remember that entry animations gate on presentation, never mount. See [Lifecycle & events](/docs/framework/lifecycle).