# 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

# Styling & Mobile Design

Dark mode, safe areas, scroll behavior, motion, and touch — the platform conventions that make a paywall feel native inside a webview.

Paywalls render inside a native webview on a phone. Two things decide whether one feels native: reproducing your design exactly, and following the platform conventions — Apple's HIG and their Android equivalents — that users feel but never name. This page collects the conventions; treat them as working practices, with your design reference always winning over any rule here.

## The design is the contract

* **Build 1:1.** Spacing, sizing, weights, colors, and effects come from the design, not from habit. Measure the design at logical points — a screenshot at device width — instead of eyeballing, and compare your build against it side by side before calling it done.
* **Add nothing the design doesn't show.** No extra links, badges, footnotes, or affordances, however well-intentioned. If something seems missing — a restore button, a legal link — raise it with your designer rather than quietly adding it.
* **Effects are design decisions, not defaults.** Shadows, gradients, borders, blurs, and radii belong to the design system of the paywall you're building. If the design is flat, build flat; if it's soft and elevated, match that.

## Dark mode

The device decides, and the framework maintains a `dark`/`light` class on `<html>`. Style with plain CSS and write no wiring:

```css
:root { --bg: #fdfef6; --fg: #0c0b0a; }
:root.dark { --bg: #1c1b19; --fg: #fdfef6; }
```

> **Warning:** Don't use `@media (prefers-color-scheme: dark)` as the mechanism. The media query can't see what the device reports through the SDK and doesn't respond to the studio's theme toggle — a paywall styled that way looks right on your machine and wrong on the device. The `:root.dark` class is the mechanism.

Using Tailwind? Redefine the `dark:` variant onto the class so it follows the SDK instead of the media query:

```css
@custom-variant dark (&:where(.dark, .dark *));
```

The Tailwind example shows the full setup — see [Examples](/docs/framework/examples). Design both palettes even when the reference shows only one, and check both in the studio.

## Safe areas

`env(safe-area-inset-*)` resolves to **0** in previews and some webview contexts, so bare `env()` math puts controls in the status bar or under the home indicator the moment insets go missing. Always wrap in `max()` with a floor:

```css
/* fixed top chrome (close button): clears the status bar even with no env */
top: max(calc(env(safe-area-inset-top, 0px) + 10px), 60px);

/* pinned bottom chrome: clears the home indicator */
padding-bottom: max(calc(env(safe-area-inset-bottom, 0px) + 14px), 28px);
```

Around 60px is a sensible top floor and 28px a bottom floor — adjust the numbers to your design, keep the pattern. Fixed elements (close button, CTA bar) need the inset math; scrolling content instead needs enough bottom padding to clear whatever is pinned over it.

## Scrollable content

* Long content scrolls **under** pinned bottom chrome. Give the pinned footer a gradient — transparent to page background — so content fades out behind it instead of clipping to a hard edge.
* Put `pointer-events: none` on the pinned container and `pointer-events: auto` back on its interactive children, so the fade region doesn't swallow scroll gestures.
* Give the scroll content bottom padding of roughly the footer height plus the safe area, so the last row can scroll clear of the fade.
* Let the page itself scroll; don't invent nested scroll areas. The platform — and `scrollEnabled` in [config](/docs/framework/config) — owns scroll behavior.

## Motion

* **Animate functional movement only** — elements that physically travel between states: a segmented-control thumb sliding, a sheet presenting, a progress bar filling. Content that merely changes — text, list rows, a price — updates in place; it doesn't fade, slide, or stagger unless the design explicitly calls for it.
* **Press feedback is the baseline interaction**: a scale-down active state (around 0.96, fast in at \~80ms, settling out at \~200ms) on tappable elements, paired with a haptic. For most controls, that's the whole story.
* **Entry animations are opt-in per design** — and when a design has one, it gates on presentation, never mount, because paywalls are preloaded hidden. See [Lifecycle & events](/docs/framework/lifecycle).
* Honor `prefers-reduced-motion` by collapsing durations to \~1ms.

## Touch

* **Tap targets are at least 44×44pt.** A visually shorter control — a slim segmented control — can trade height when the design demands it, but width and spacing must compensate.
* **Haptics on every meaningful tap**, via [`useHaptics()`](/docs/framework/hooks#usehaptics): `light` for navigation and CTAs, `selection` for choosing between options, `success` when a purchase lands, `error` sparingly on failures. iOS fires nothing on its own inside a webview.
* **Suppress focus rings on tap-driven controls.** The `:focus-visible` heuristics misfire in webviews and previews, drawing outlines the design never asked for. Keep keyboard focus styles only where a keyboard is real, like web checkout pages.
* On controls: `-webkit-tap-highlight-color: transparent`, `touch-action: manipulation`, `user-select: none`.
* Icon-only buttons carry an `aria-label`; every control stays reachable.

## Type and rendering

* Default to the system font stack — `-apple-system, BlinkMacSystemFont, …` — unless the design specifies brand type. It's what makes a webview read as native iOS. (When the design calls for brand type, see [custom fonts in Assets](/docs/framework/assets).)
* Set `-webkit-text-size-adjust: 100%` on `html`, use antialiased smoothing, and keep body copy around 17px to match iOS body text.

## Verify like a device

In the [studio](/docs/framework/studio), before calling any paywall done:

* Both color schemes.
* The smallest supported width — 320px — through tablet.
* Every page in the flow.
* The trial-eligibility toggle, where relevant.
* Nothing overflows horizontally at any size.