# 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

# Assets

Add images, video, audio, fonts, and animations to a paywall by importing files. The build handles optimization, hosting, and caching.

Add media to a paywall by importing files from an `assets/` directory. The build handles optimization, hosting, and caching; there is nothing to configure and no upload step.

## Where assets live

Assets follow the same two-level pattern as components and messages: shared at the project root, local inside a paywall.

```ts
superwall/
├── assets/               shared across every paywall
└── paywalls/pro/
    └── assets/           this paywall's own
```

> **Warning:** Every asset belongs in an `assets/` directory — `superwall/assets/` for shared files, `superwall/paywalls/<id>/assets/` for one paywall's own. If a large asset lives anywhere else, the build fails and names the file.

## Use an image

Import the file and use it like any URL:

```tsx
import hero from "@/assets/hero.jpg";          // shared: superwall/assets/
import badge from "../assets/badge.png";        // this paywall's own

<img src={hero} alt="" />
```

CSS `url()` works the same way. Imports typecheck because of the generated `superwall.d.ts` — one more reason to [commit it](/docs/framework/project-structure).

Supported out of the box:

| Kind           | Formats                                                   |
| -------------- | --------------------------------------------------------- |
| Images         | `png` `jpg` `jpeg` `webp` `avif` `gif` `svg` `ico` `apng` |
| Video          | `mp4` `webm` `mov` `m4v`                                  |
| Audio          | `mp3` `m4a` `aac` `wav` `ogg`                             |
| Fonts          | `woff2` `woff` `ttf` `otf`                                |
| Animation & 3D | `lottie` `riv` `glb`                                      |

`?url`, `?raw`, and `?inline` import suffixes work too, as do CSS modules.

## How hosting works

You never choose where an asset is served from — the build decides, and nothing about your code changes either way:

* **Video, audio, and fonts** are always served from Superwall's CDN, whatever their size. Video streams properly instead of being carried by the paywall, and one upload is reused across every version of every paywall.
* **Images** embed in the paywall when small and move to the CDN when large.

```tsx
import promo from "../assets/promo.mp4";

<video src={promo} autoPlay muted loop playsInline />
```

## Custom fonts

A relative-path `@font-face` is the whole setup:

```css
@font-face {
  font-family: "Manrope Custom";
  font-display: swap;
  font-weight: 200 800;
  src: url("../assets/manrope-latin.woff2") format("woff2-variations");
}

:root { --sans: "Manrope Custom", ui-sans-serif, system-ui, sans-serif; }
```

A few habits keep fonts cheap:

* **Subset before you ship.** A full variable font carries alphabets the paywall will never render — latin-only Manrope is around 24 kB against roughly 90 kB for the whole family.
* **Ship woff2.** Anything older is bytes for nothing you support.
* **Google Fonts go in a CSS `@import`**, at the top of the stylesheet — never React-rendered `<link>` tags. The stylesheet ships in the page itself, so the browser finds the `@import` immediately; a rendered `<link>` waits for JavaScript to run first, and the text flashes.
* One family plus one mono is a good budget.

The custom-fonts [example](/docs/framework/examples) shows a local file and a Google Fonts import side by side.

## Lottie

Two ways to ship a Lottie animation, with different trade-offs:

```tsx
// 1. Animation JSON — embedded in the paywall. Offline-proof, zero requests.
//    Best for small animations.
import spinner from "@/assets/spinner.json";

// 2. A .lottie file — served from the CDN and pre-cached on device by the
//    SDK before the paywall opens. Best for bigger animations.
import intro from "@/assets/intro.lottie";
```

## Rive

`.riv` files load like any asset, plus one required setup step:

```tsx
import { useRive, RuntimeLoader } from "@rive-app/canvas";
import riveWasm from "@rive-app/canvas/rive.wasm?url";
import smiley from "../assets/smiley.riv";

RuntimeLoader.setWasmUrl(riveWasm);
RuntimeLoader.setWasmFallbackUrl(null);

const { RiveComponent } = useRive({ src: smiley, stateMachines: "State Machine 1", autoplay: true });
```

> **Warning:** Rive fetches its WebAssembly engine from a CDN by default, and published paywalls cannot reach external CDNs. Bundle the wasm with the `?url` import as above, and null the fallback so a failure stays loud rather than silently retrying a CDN that will never answer.

Also pass the file's **real state-machine name** — naming one that doesn't exist leaves a blank canvas and no error. The with-rive [example](/docs/framework/examples) is the reference.

## Multi-page flows

Nothing to do — while the user is on the current page, the next pages' images, video, and fonts warm automatically. `.lottie` and `.riv` files go further: the SDK pre-caches them on device before the paywall even opens.

## Keep it light

* Big imagery is fine — it's served from the CDN and cached, not carried by the paywall itself.
* Compress and size media for a phone screen; every open pays for what the paywall loads.
* Pushing files over 50 MB warns — every future clone of the source pays for them — but nothing is capped.