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 whyThe 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.
| Flag | What 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-source | Replace 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 devprints. - A product in
config.tsdoesn'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 paywallChoosing 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-upgradeDeleting 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 3Until 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.lockrecords no sync point to merge from. Pull the latestsuperwall.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 promoteBecause superwall.lock is committed, CI pushes to exactly the same paywalls as every developer machine.
How is this guide?
The Studio
Preview every paywall locally with superwall dev: devices, themes, locales, live variables, and purchase, restore, permission and callback outcomes you choose.
Migrating from the editor
Rebuild a paywall made in Superwall's visual editor as a framework paywall: what the CLI scaffolds, what an agent rebuilds, how you review it against the original, and how the switch happens.