# 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

# Server-side gating

Enforce entitlements on your backend, where users cannot tamper with them.

> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.

## Why gate on the server

The browser SDK keeps subscription state locally so your UI can react instantly. That state is readable and writable from DevTools. Client-side checks control what the browser shows; they do not control access. Anything that costs money or exposes real data must be checked on the server.

Two packages do that. They differ in whether the check needs a network call.

| Package             | How it checks                                                                     | Use when                                       |
| ------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------- |
| `@superwall/server` | Calls Superwall's `/entitlements` endpoint, with caching                          | You want the simplest correct thing            |
| `@superwall/verify` | Verifies a signed token against bundled keys, no network on the steady-state path | You want per-request checks with no round trip |

## `@superwall/server`

Create an instance, tell it how to find the current user, and gate routes with `requires`.

```ts
import { Superwall } from "@superwall/server";

const sw = Superwall<express.Request>({
  apiKey: process.env.SUPERWALL_API_KEY!,
  userId: (req) => req.session?.userId ?? null,
});

app.get("/api/export", sw.requires("pro"), exportHandler);
```

The middleware is Connect-style and works with Express and anything with the same shape. Pass your request type as the generic parameter, or `req` infers as `unknown`.

> **Warning:** **The `userId` extractor is the trust boundary.** Read it from an authenticated session. Reading it from the request body, a query string, or an unverified header lets any caller claim to be any user.

### Describing what is required

```ts
sw.requires("pro");                      // one entitlement
sw.requires(["pro", "team"]);            // all of them
sw.requires({ all: ["pro", "team"] });   // the same, explicitly
sw.requires({ any: ["pro", "team"] });   // at least one
```

### Options

| Option        | Purpose                                                                                           |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `apiKey`      | Your Superwall API key. Keep it in the environment, not in source.                                |
| `userId`      | How to pull the user out of a request. Overridable per `requires` call.                           |
| `cache`       | Cache adapter and TTL for entitlement lookups. Defaults to 60 seconds, in-memory, single-process. |
| `environment` | Network environment. Defaults to `"release"`.                                                     |
| `timeoutMs`   | Timeout for calls to Superwall. Defaults to `5000`.                                               |
| `onRequest`   | Called after a completed check. Useful for tracing.                                               |

`requires` also takes `allowAnonymous`, which defaults to `false`. A request with no resolvable user is rejected. Setting it to `true` passes those requests through.

### What a rejection looks like

By default a blocked request gets `403` with `{ error: "entitlement_required", entitlement }`. Pass `onUnauthorized` to `requires` to customize it. It receives the missing entitlements and a `reason` of `"no_user_id"` or `"not_entitled"`.

If Superwall cannot be reached, the middleware calls `next(err)` rather than rejecting with a 403, so it surfaces through your framework's error handler, typically as a 500. It fails closed either way.

### Cache staleness

Entitlement lookups are cached for 60 seconds by default, so a revocation can take that long to take effect. Call `sw.invalidate(userId)` or `sw.invalidateAll()` when you need it immediately. The default cache is in-memory and single-process. Supply a Redis or KV adapter for multi-instance deployments.

### Checking without middleware

```ts
if (await sw.userHas(userId, "pro")) {
  // …
}
```

> **Note:** The user ID comes first. Both parameters are strings, so reversing them still typechecks. It
> looks up a user named `"pro"`, finds nothing, and denies everyone.

## `@superwall/verify`

The client holds a Superwall-signed token describing its entitlements. Send it to your backend and verify the signature. A valid signature proves Superwall issued exactly those entitlements, with no call to Superwall needed.

Read the token in the browser:

```ts
sw.entitlementsToken.value;            // reactive
sw.purchases.getEntitlementsToken();   // snapshot
```

Verify it on the server. `verifyEntitlements` throws on any failure and never returns a partial result, so the call belongs in a `try/catch`:

```ts
import { verifyEntitlements, VerifyError } from "@superwall/verify";

try {
  const result = await verifyEntitlements(tokenFromClient, {
    publicApiKey: process.env.SUPERWALL_PUBLIC_API_KEY!,
  });

  if (!result.entitlements.some((e) => e.identifier === "pro")) {
    return res.status(402).end();
  }
} catch (err) {
  if (err instanceof VerifyError) return res.status(401).end();
  throw err;
}
```

> **Note:** The token's entitlements use `identifier`, not `id`. This is a different type from the client
> SDK's `Entitlement`, which uses `id`. See [Tracking subscription
> state](/docs/web/quickstart/tracking-subscription-state).

There are convenience helpers for the common shapes, both taking the token first:

```ts
import { userHasEntitlement, userHasAnyEntitlement } from "@superwall/verify";
```

### Errors

Verification failures are typed. Catch `VerifyError` for any of them, or narrow to `InvalidSignatureError`, `ExpiredError`, `AudienceMismatchError`, `MalformedTokenError`, or `KeyUnavailableError`.

### Expiry

A token carries its own expiry, and each entitlement inside it carries an `expiresAt` of its own. An entitlement can lapse while the token wrapping it is still valid.

The convenience helpers account for this, so prefer them over iterating the array yourself:

```ts
if (!(await userHasEntitlement(tokenFromClient, "pro", { publicApiKey }))) {
  return res.status(402).end();
}
```

If you read `result.entitlements` directly, check `expiresAt`. It is epoch milliseconds, and `null` means lifetime.

> **Warning:** Run verification on the server. A check that runs in the browser can be bypassed in the browser.