Push, Promote & Publish

Ship paywalls with git semantics: push seals an immutable version, promote points production at it, publish does both.

Shipping has git semantics on purpose: push saves, promote ships. Every push mints a sealed, immutable version; nothing your users see changes until promote points production at it.

superwall push                    # build + version. Production untouched.
superwall promote                 # point production at the latest push
superwall publish                 # push + promote in one step
superwall publish -m "Q3 test"    # record why

The scaffolded project mirrors these as package scripts (dev, push, promote, ship).

superwall push

Builds every paywall, versions the changed ones, and leaves production alone. Re-running with nothing changed is a no-op.

FlagWhat it does
--id <id>Limit the push to one paywall (repeatable)
--platform <p>Limit the push to one platform (repeatable), see below
--rename <old>=<new>Declare a directory rename (see below)
-m <note>Record why this version exists
--overwrite-sourceReplace the app's saved source with yours when it can't be merged, see Source snapshots

The first push binds each paywall, creating it on Superwall if needed, and records the binding in superwall.lock. Commit that file: it's what makes every machine and CI push to the same paywalls. After that, push always updates the same paywall; no IDs ever appear in your code. If the app already has a headless paywall with the same id that superwall.lock doesn't bind, push never takes it over on its own: an interactive push asks, and any other run stops and names it. Add the printed line to superwall.lock if it's yours, or rename the directory to create a new paywall.

A push refuses, before anything is written, when:

  • A selected paywall has diagnostics. Publishing is immutable; fix the named problems first. They're the same warnings superwall dev prints.
  • A product in config.ts doesn't exist on the dashboard. Every variable on it would be undefined on device. Create the products first, see Products and the CLI reference.
  • A directory rename is unresolved (below).

Renames

Renaming a paywall directory is detected, never guessed. Interactively, push asks:

? `pro-upgrade` is not in superwall.lock. Is it a new paywall, or renamed?
  › Renamed from plus-upgrade   paywall 208540
    Create a new paywall

Choosing the rename keeps the live paywall attached to the new directory. In CI there's no one to ask, so declare it, anything unresolved stops the push rather than silently creating a duplicate:

superwall push --rename plus-upgrade=pro-upgrade

Deleting a paywall directory never blocks a push: the dashboard paywall keeps serving, and restoring the directory re-binds it.

Every pushed version is previewable in the dashboard: opening the paywall there opens the hosted studio on the live version, with the others one switch away.

Several platforms

One project can push to several platforms (an iOS app, an Android app, a web app) as long as the apps belong to the same Superwall project. Each paywall's config.ts lists the platforms it's for:

export default definePaywall({ platforms: ["ios", "android"], products: { … } });

superwall.lock binds one Superwall app per platform under apps. The first push that meets a new platform binds it: when your account has exactly one app of that platform, it's picked for you; when it has several, push asks; when it has none, push says so. In CI there's no one to ask, so bind it once interactively, or add the line yourself, "web": "<app id>", with ids from superwall apps list.

A paywall pushed to two platforms becomes two dashboard paywalls, each with its own versions, and the output names them:

plus-upgrade · ios
  Linked to paywall 208551
  Pushed version 4 (3f2a9c1)
plus-upgrade · android
  Linked to paywall 208560
  Pushed version 2 (8b71e04)

--platform <p> narrows push, promote, and publish to one platform (the bound app's id works too), and promote --version needs it when the paywall is on several:

superwall push --platform android
superwall promote --id plus-upgrade --platform ios --version 3

Until a project is bound on a second platform, platforms is optional and everything pushes to the one app. Once it is, every paywall has to say which platforms it's for; a push stops on a silent one rather than create a paywall on the wrong app.

Source snapshots

Every push also saves your superwall/ source to the app's source repo on Superwall, so the exact code each version was built from is recoverable. The -m "why" note is recorded on that source commit, it only lands when the source actually changed.

The repo is shared by everyone who pushes to the app. A push applies your changes on top of whatever teammates have published since your last push, so two people working on different paywalls never undo each other. The commit each app was last synced to is kept in superwall.lock, under sources; commit the file, since it's the base the next push merges from. A push stops and names the files, rather than overwriting someone's work, when:

  • You and a teammate changed the same file. Pull their commits into the project, then push again.
  • The repo has changes this project never synced with, and superwall.lock records no sync point to merge from. Pull the latest superwall.lock, then push again.

When you mean to replace the saved source with yours, pass --overwrite-source to push or publish.

A version is only promoted once its source has landed. If a push creates a version but its source doesn't land, superwall.lock remembers it, and the next push finishes that version's source before anything else instead of skipping it as already pushed. Until then promote refuses that version (version N has no source yet), and publish finishes the source before it promotes.

What never leaves your machine: .env files, node_modules/, .superwall/, .git/, key and credential files (*.p8, *.p12, *.pem, *.key, keystores, .npmrc, .netrc, google-services.json, GoogleService-Info.plist), and anything superwall/.gitignore lists. A source tree over 250 MB stops the push before any version is created, naming the largest files.

If any import reaches outside the project directory, the push warns and records the version as non-portable. The pushed source can't be rebuilt elsewhere. The warning names the first five offenders, then counts the rest. Either copy the shared code into superwall/components/, or install it from a package registry. See Project structure.

superwall promote

Points production at a pushed version. Promote never rebuilds. It only moves the live pointer, so it's instant, and rollback is the same move in reverse:

superwall promote                              # latest push, every paywall
superwall promote --id plus-upgrade            # just one
superwall promote --id plus-upgrade --version 5
# → Rolled back version 7 → 5
superwall promote --id plus-upgrade --platform ios --version 5   # a paywall on several platforms

--version/-v (with a single --id, plus --platform when that paywall is on several platforms) picks a specific version. Pinning forward and rolling back are the same operation.

superwall publish

Push + promote in one step. It also warns about other paywalls that are pushed-but-not-live, so nothing ships half-forgotten.

push and publish both require git. The source snapshot is part of each, and without git nothing is created.

CI

Interactive machines authenticate once with superwall login. In CI, set SUPERWALL_API_KEY (an sk_… key) in the environment. superwall/.env works locally and is gitignored. dev needs no login at all.

A typical CI ship step:

superwall push --rename old=new -m "$COMMIT_MESSAGE"   # renames declared, reason recorded
superwall promote

Because superwall.lock is committed, CI pushes to exactly the same paywalls as every developer machine.

How is this guide?

On this page