# 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

# Localization

Ship a paywall in multiple languages by adding one file per locale — no registration, no wiring.

Ship a paywall in multiple languages by adding one file per locale. The filename is the locale, and the device picks which one renders — no registration, no wiring.

## Add locales

Message catalogs live in `messages/` directories, at the same two levels as components and assets:

```ts
superwall/
├── messages/                 shared by every paywall
│   ├── en.ts
│   └── de.ts
└── paywalls/pro/
    └── messages/             this paywall's own
        ├── en.ts
        └── fr.ts
```

Each file default-exports a nested object:

```ts
// paywalls/pro/messages/fr.ts
export default {
  paywall: {
    title: "Passez à Pro",
    cta: "S'abonner · {price}",
    perMonth: "{price} par mois, facturé annuellement",
  },
} as const;
```

A paywall's own catalog layers over the shared one — it overrides the keys it names and inherits the rest. A locale can exist in either layer or both.

If your fallback language isn't English, set it in `config.ts`:

```ts
localization: { defaultLocale: "en" },
```

## Use the strings — `useTranslation()`

```tsx
const { t, locale, setLocale, locales } = useTranslation();

<h1>{t("paywall.title")}</h1>
<button>{price ? t("paywall.cta", { price }) : t("paywall.ctaBare")}</button>
<button aria-label={t("paywall.close")}>×</button>
```

* **`t(key, values?)`** — the translated string for the active locale. Interpolation is `{name}` in the catalog with `t(key, { name: value })` at the call site.
* **`locale`** — the active locale, resolved from the device. Resolution is specific-to-general: `pt-BR` matches a `pt-BR` catalog first, then `pt`, then the default locale.
* **`setLocale(locale)`** — override the device; `setLocale(undefined)` returns to auto-detection. This is for previews and tests — on device, the system setting is the truth.
* **`locales`** — every locale that has a catalog.

## How fallbacks behave

* A key missing from the active locale falls back to the default locale **per key** — a partial translation stays usable while it's being finished.
* An unknown key renders as itself, so `t()` never breaks. The flip side: **key typos are invisible at runtime** — nothing throws, the key just shows up on screen. Check your copy in [the studio](/docs/framework/studio) with its locale switcher.
* Guard interpolations on the value existing, with a bare-key fallback — as in the CTA above. Never render "Subscribe · undefined".

## The rules

* **Never put a price in a catalog.** Prices are localized by the store — the SDK delivers the right currency and format for the user's region. Interpolate them: `"Subscribe · {price}"`. See [Products](/docs/framework/products).
* **No language picker on device.** The locale is the person's system setting; preview other locales with the studio's locale switcher.
* **Copy expands.** German runs long — size nothing to fit English.
* Product `period` and `periodly` variables ("yearly" → "jährlich") localize automatically in 44 languages, independent of your catalogs.
* A single-locale paywall needs none of this — plain strings in JSX are fine until the second locale arrives.

> **Note:** There is no plural engine — no ICU, no `_one`/`_other` suffixes. Write around plurals, or fork on the count yourself.

The localization [example](/docs/framework/examples) shows four locales, both catalog layers, and guarded interpolation.