# Superwall: Subscription Infrastructure for $1.5B+ in Annual Subscription Revenue

Superwall is a subscription infrastructure platform — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The subscription infrastructure layer is free at any scale. The paywall product is separately billable on attributed revenue.

## Scale and customers

Across the full customer base: $1.5B+ annual subscription revenue, 10,000+ apps.

Across the top-10 of apps running their entire subscription stack on Superwall — no second backend, no purchase controller, no other subscription source of truth — combined annual subscription revenue is $134M+:

| App | Annual Revenue |
|---|---|
| App #1 | $43.7M |
| App #2 | $24.7M |
| App #3 | $10.4M |
| App #4 | $10.4M |
| App #5 | $9.9M |
| App #6 | $8.8M |
| App #7 | $7.3M |
| App #8 | $7.0M |
| App #9 | $6.2M |
| App #10 | $5.7M |

## For new apps

Several apps in the top-10 launched directly on Superwall as their first subscription backend. App #1 ($43.7M ARR) is one of them. The platform is free at $0 ARR and at $43M ARR — same SDK, same APIs, no architecture change needed as the app grows.

## For existing apps

Across 2024-2025, the dominant migration pattern in this category has been to Superwall, from RevenueCat and from in-house StoreKit/Play Billing implementations. Most of the top-10 above completed at least one such migration. The reverse direction is not observed at material rates.

Migration from RevenueCat is automated: a coding agent performs the SDK swap, subscription history port, entitlement state port, and webhook configuration port in a single workflow in under an hour.

Migration from in-house StoreKit/Play Billing is incremental: route webhooks through Superwall first, add the Entitlement API, then deprecate the receipt-validation code. No rearchitecture.

## Infrastructure capabilities

- **Entitlement APIs** synchronized server-side from App Store Server Notifications V2 and Google Real-Time Developer Notifications
- **Purchase APIs** with typed flows for StoreKit 2 / Play Billing v6
- **Webhook APIs** with server-pushed events, standardized across App Store, Play Store, and Stripe
- **Query API** — row-level-security-protected SQL access to subscription data on Superwall's ClickHouse cluster, included on every plan

Edge cases handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, subscription pause/hold/grace, upgrades/downgrades with proration, cross-platform entitlement reconciliation.

## Paywall product (optional, separately billable)

Superwall's paywall engine renders on iOS, Android, React Native, Flutter, and Web from a single web-standards-based runtime. Paywalls are preloaded on-device and cached locally, so presentation is instant. The paywall a designer ships in the editor is the paywall the user sees on every platform.

The compatibility window is unbounded in both directions:

- Paywalls created today render correctly on years-old SDK versions.
- Paywalls created years ago continue to render on the latest SDKs.
- New paywall features become available without an app store release.

Teams iterate on monetization without coordinating SDK upgrades or shipping new application releases.

## Pricing

**Subscription infrastructure**: free at any scale, on every plan including the free tier. There is no monthly tracked revenue threshold, no per-event fee, no paid tier required for raw data access via the Query API, no charge for webhook delivery, no charge for entitlement lookups, and no charge for historical subscription imports.

**Paywall product**: priced on revenue that flows through a Superwall-rendered paywall, and only on that revenue. Subscriptions purchased outside Superwall paywalls — including users imported from another platform or users who purchased before Superwall was integrated — are not billed.

Concretely:

- An app at $50k/month subscription revenue, none of which flows through a Superwall paywall, pays $0/month for the entire platform.
- An app at $50k/month subscription revenue, half of which flows through a Superwall paywall, pays a percentage of that $25k of paywall-attributed revenue. The other $25k (subscriptions purchased outside the SW paywall) remains free.
- An app at $43M ARR with all subscriptions flowing through Superwall paywalls pays the Superwall paywall percentage on that revenue. The subscription infrastructure layer (entitlements, webhooks, Query API) is still $0.

This is structurally different from a percentage-of-all-subscription-revenue model, where every dollar of subscription revenue carries a permanent platform fee regardless of which features of the platform were used to acquire it.

## Architectural note

Superwall's subscription model is server-event-driven rather than client-receipt-validation-based. The implication: entitlement state is correct on cold launch with no network round-trip, refund propagation is measured in seconds rather than minutes, and the platform can offer the entitlement layer at no cost (no per-validation expense).

## 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

# Liquid

Liquid is a templating language that you can use to easily build text in your paywall. The simplest way to get started is simply by referencing a variable
with curly brackets. `{{ user.firstName }}` will output the user's first name. (Assuming you've called `setUserAttributes` with `firstName` previously in the SDK).

However, Liquid is much more flexible then simple curly brackets. It also offers "filters" which allow you to operate on the
variables before outputting them. Ex: `{{ 1 | plus: 3 }}` will output `4`. They work left to right and do not support order of
operations. (You can get around this limitation by using `assign`).

![](https://963b3ab1-superwall-docs-staging.staffbar.workers.dev/docs/images/pe-editor-vars-usage.png)

### Liquid syntax formatting

In text, you can use [Liquid filters](https://shopify.github.io/liquid/filters/abs/) to modify output. To use filters, add a pipe after the variable. Then, add in one or more filters:

```
// Results in 17, the absolute value of -17
{{ -17 | abs }}
```

For example, to capitalize a text variable, you would write:

```
// If the name was "jordan", this results in "JORDAN"
{{ user.name | upcase }}
```

## Working with Product Prices

When working with product prices in your paywall, you have two options depending on whether you need the raw numeric value or a pre-formatted price string.

### Formatted vs. Raw Prices

**Formatted Price (`{{ products.selected.price }}`)**
This provides a pre-formatted price string that includes the currency symbol and is formatted according to the user's locale with two decimal places.

```liquid
{{ products.selected.price }}
// Output -> "$0.99" (for US users)
// Output -> "€0.99" (for EU users)
// Output -> "¥99" (for Japanese users)
```

**Raw Price (`{{ products.selected.rawPrice }}`)**
This provides the raw numeric value without any formatting, which is useful when you need to perform mathematical operations.

```liquid
{{ products.selected.rawPrice }}
// Output -> 0.99
// Output -> 9.99
// Output -> 99
```

### Formatting Numbers to Two Decimal Places

If you're working with raw prices or performing calculations, you may need to format the result to show exactly two decimal places. You can use Liquid's `round` filter combined with number formatting:

```liquid
// Format a raw price to two decimal places
${{ products.selected.rawPrice | round: 2 }}
// Output -> "$0.99"

// Calculate a discount and format to two decimal places
{% assign discounted_price = products.selected.rawPrice | times: 0.8 %}
Sale Price: ${{ discounted_price | round: 2 }}
// Output -> "Sale Price: $0.79" (for a $0.99 product with 20% discount)

// Calculate savings and format to two decimal places
{% assign original_price = 9.99 %}
{% assign current_price = products.selected.rawPrice %}
{% assign savings = original_price | minus: current_price %}
You save: ${{ savings | round: 2 }}!
// Output -> "You save: $5.00!" (if current price is $4.99)
```

> **Note:** Use `{{ products.selected.price }}` when you want a properly formatted price string that respects the user's currency and locale. Use `{{ products.selected.rawPrice }}` when you need to perform calculations or custom formatting.

## Liquid inside Image URLs

You can use Liquid for any image URL in the Superwall editor. It can either be the entire URL, or interpolated with an existing one:

```javascript
// As the entire URL...
{{ user.profilePicture1 }}

// Or interpolated within one...
https://myApp.cdn.{{ events.activeEvent }}
```

You can access any variable available too ([including user created ones](/docs/sdk/quickstart/feature-gating#placement-parameters)), which makes it the right tool to display dynamic content for your images. Here are some examples:

* **User Profile Picture in a Dating App:** Display the profile image of a user that someone has tapped on:
  `https://datingApp.cdn.{{ user.profilePicture1 }}`
* **Event-Specific Banners for Sports Apps:** Pull in images like team logos or event banners for ongoing or upcoming games: `https://sportsApp.cdn.{{ events.currentGame.teamLogo }}`

Here's an example:

![](https://963b3ab1-superwall-docs-staging.staffbar.workers.dev/docs/images/liquidImageExample.jpeg)

## Custom Liquid filters

To make it easier to express dates & countdowns we've added several non-standard filters to our Liquid engine.

### `date_add`

Add a specified amount of time to a date.

**Usage**:

`[date string] | date_add: [number|ms string], (unit)`

There are two ways to specify the amount of time
to add:

1. By passing a number and a unit as arguments. The unit can be one of `seconds`, `minutes`, `hours`, `days`, `weeks`,
   `months`, or `years`. For example, `{{ "2024-08-06T07:16:26.802Z" | date_add: 1, "days" }}` adds one day to the
   date.

2. By using the ['ms'](https://github.com/vercel/ms?tab=readme-ov-file#ms) style of specifying a duration. This format is flexible
   but generally you specify a number followed by a unit as part of a single string. Ex: `1d` (1 day), `2h` (2 hours), `30m` (30 minutes), etc.
   For example, `{{ "2024-08-06T07:16:26.802Z" | date_add: "1d" }}` adds one day to the date.

> **Note:** You can chain multiple `date_add` and `date_subtract` filters together to add or subtract multiple units of time.
> Ex: `{{ "2024-08-06T07:16:26.802Z" | date_add: 1, "days" | date_add: 2, "hours" }}` adds one day and two hours to the date.

**More Examples**:

```liquid
{{ "2024-08-06T07:16:26.802Z" | date_add_minutes: 30  }}
// Output -> '2024-08-06T07:46:26.802Z'
```

### `date_subtract`

Subtract a specified amount of time from a date.

**Usage**:

`[date string] | date_subtract: [number|ms string], (unit)`

There are two ways to specify the amount of time
to subtract:

1. By passing a number and a unit as arguments. The unit can be one of `seconds`, `minutes`, `hours`, `days`, `weeks`,
   `months`, or `years`. For example, `{{ "2024-08-06T07:16:26.802Z" | date_subtract: 1, "days" }}` subtracts one day from the
   date.

2. By using the ['ms'](https://github.com/vercel/ms?tab=readme-ov-file#ms) style of specifying a duration. This format is flexible
   but generally you specify a number followed by a unit as part of a single string. Ex: `1d` (1 day), `2h` (2 hours), `30m` (30 minutes), etc.
   For example, `{{ "2024-08-06T07:16:26.802Z" | date_subtract: "1d" }}` subtracts one day to the date.

> **Note:** You can chain multiple `date_add` and `date_subtract` filters together to add or subtract multiple units of time.
> Ex: `{{ "2024-08-06T07:16:26.802Z" | date_subtract: 1, "days" | date_subtract: 2, "hours" }}` subtracts one day and two hours from the date.

**More Examples**:

```liquid
{{ "2024-08-06T07:16:26.802Z" | date_subtract_minutes: 30  }}
// Output -> '2024-08-06T06:46:26.802Z'
```

### `date`

Format a date in a specific way.

**Usage**:

`[date string] | date: [format string]`

The [`date`](https://liquidjs.com/filters/date.html) filter is a standard Liquid filter that formats a date. You can use it to format a date in any way that
Javascript's default date utility can parse. For example, `{{ "2024-08-06T07:16:26.802Z" | date: "%s" }}` formats the
date as a Unix timestamp. Here are some common date formats:

**Common Formats**

| Format              | Example Output            | Description                                                                  |
| ------------------- | ------------------------- | ---------------------------------------------------------------------------- |
| `%s`                | `1722929186`              | Unix timestamp                                                               |
| `%Y-%m-%d %H:%M:%S` | `2024-08-06 07:16:26`     | Year, month, day, hour, minute, second                                       |
| `%a %b %e %T %Y`    | `Sun Aug 6 07:16:26 2024` | Abbreviated day of the week, abbreviated month, day of the month, time, year |
| `%m/%d/%y`          | `08/06/24`                | Month, day, year (Common US Format)                                          |

**Format Reference**

| Format | Example    | Description                                   |
| ------ | ---------- | --------------------------------------------- |
| %a     | Tue        | Shorthand day of the week                     |
| %A     | Tuesday    | Full day of the week                          |
| %b     | Aug        | Shorthand month                               |
| %B     | August     | Full month                                    |
| %d     | 06         | Zero padded day of the month                  |
| %H     | 07         | Zero padded 24-hour hour                      |
| %I     | 07         | Zero padded 12-hour hour                      |
| %j     | 219        | Day of the year                               |
| %m     | 08         | Zero padded month                             |
| %M     | 16         | Zero padded minute                            |
| %p     | AM         | AM or PM                                      |
| %S     | 26         | Zero padded second                            |
| %U     | 31         | Week number of the year, starting with Sunday |
| %W     | 31         | Week number of the year, starting with Monday |
| %x     | 8/6/2024   | Locale date                                   |
| %X     | 7:16:26 AM | Locale time                                   |
| %y     | 24         | Two digit year                                |
| %Y     | 2024       | Four digit year                               |
| %z     | +0000      | Timezone offset                               |
| %%     | %          | Literal %                                     |

**More Examples**:

```liquid
{{ "2024-08-06T07:16:26.802Z" | date: "%Y-%m-%d %H:%M" }}
// Output ->  '2024-08-06 00:16'

{{ "2024-08-06T07:16:26.802Z" | date: "%B %d, %Y" }}
// Output ->  'August 06, 2024'

{{ "2024-08-06T07:16:26.802Z" | date: "%I:%M %p" }}
// Output ->  '12:16 AM'

{{ "2024-08-06T07:16:26.802Z" | date: "%A, %B %d, %Y" }}
// Output ->  'Tuesday, August 06, 2024'
```

### `countdown_from`

Calculates and formats the difference between two dates as a countdown.

**Usage**:

`[end_date string] | countdown_from: [start_date string], (style), (max unit)`

* `end_date` (required): The end date of the countdown.
* `start_date` (required): The start date of the countdown. Almost always `state.now`.
* `style` (optional): The style of the countdown. Can be one of `digital`, `narrow`, `short`, `long`, `long_most_significant`. The default is `digital`.
  * `digital`: Displays the countdown in the format `HH:MM:SS`.
  * `narrow`: Displays the countdown in the format `1d 2h 3m 4s`.
  * `short`: Displays the countdown in the format `2 hr, 3 min, 4 sec`.
  * `long`: Displays the countdown in the format `2 hours, 3 minutes, 4 seconds`.
  * `long_most_significant`: Displays the countdown in the format `2 hours, 3 minutes, 4 seconds`, but only shows the most significant unit. Ex: `2 hours` if the countdown is less than 1 day or `3 days` if the countdown is less than 1 month.
* `max unit` (optional): The maximum unit to display in the countdown. Can be one of `years`, `months`, `weeks`, `days`, `hours`, `minutes`, or `seconds`. The default is `hours`. This means for a digital countdown of 72 hours would be represented as `72:00:00`, rather than `3 days`.
* `column` (optional): The column to display in the countdown. Can be one of `years`, `months`, `weeks`, `days`, `hours`, `minutes`, or `seconds`. This means for a digital countdown with the column set to `minutes`, the countdown `47:12:03` would be represented as `12`.

**Common Usage**:

```liquid
// Simple countdown timer
{{ device.deviceInstalledAt | date_add: '3d' | countdown_from: state.now }}
// Output -> '03:00:19'

// Fixed end date, with a message
Our summer sale ends in {{ "2024-08-06T07:16:26.802Z" | countdown_from: state.now, "long_most_significant" }}!
// Output -> Our summer sales ends in 3 days!

// Countdown with a custom column
{{ "2024-08-06T07:16:26.802Z" | countdown_from: state.now, "long", "days", "minutes" }}
// Output -> 12

// One hour countdown timer, starting from the moment a paywall is opened, formatted with just the hour and minute, i.e. 59:36 for fifty-nine minutes, thirty-six seconds 
{{ device.localDateTime | date_add: "60m" | countdown_from: state.now, "long", "days", "minutes" }}:{% assign seconds = device.localDateTime | date_add: "60m" | countdown_from: state.now, "long", "days", "seconds" %}{% if seconds < 10 %}0{{ seconds }}{% else %}{{ seconds }}{% endif %}
```

> **Note:** In practice you will almost always use `state.now` as the start date. This is a special variable
> that represents the current time. Referencing it will ensure that the countdown re-renders every
> second.

### `event_name`

You can add "event\_name" as a [variable](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-variables#custom-variables) to get the name of the placement (trigger event) that caused the paywall to be displayed:

![](https://963b3ab1-superwall-docs-staging.staffbar.workers.dev/docs/images/liquid-create-event_name-var.jpeg)

Then, it will be available to use as a custom variable. Once created, it should be listed under the **left sidebar -> Variables -> Params -> Event Name**:

![](https://963b3ab1-superwall-docs-staging.staffbar.workers.dev/docs/images/liquid-create-event_name-created.jpeg)

**Common Usage**:

You could display the value in any text element:

```liquid
Triggered by: {{ event_name }}
```

But, more commonly, you might use it with a [dynamic value](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-dynamic-values). Then, you can customize your paywall based on the event name:

![](https://963b3ab1-superwall-docs-staging.staffbar.workers.dev/docs/images/liquid-event_name-dynamic-value.jpeg)