Troubleshooting
Common CLI errors and runtime surprises, what each one means and how to fix it.
The most common failures, in two groups: errors the CLI prints, and runtime behavior that surprises people the first time.
CLI errors
Not a superwall project
The CLI couldn't find a project from where you ran it. Run commands from your app root or from inside superwall/, and check that the project's package.json depends on superwall. See Project structure.
…package.json is named "superwall"
Your project's package.json has "name": "superwall", which shadows the framework import. Nothing in the project can import from superwall anymore. Rename the package; superwall create names it after your app for exactly this reason.
No superwall framework found
The project exists but its dependencies aren't installed, or superwall isn't among them. Run bun add superwall (or npm install superwall) inside the project directory.
This product / These N products do not exist on Superwall
A config.ts names a product identifier the dashboard has no product for. The push refuses because every variable on that product would be undefined on device. Either fix the identifier, or create the products, from the dashboard, or with superwall products create from the CLI. See Products and the CLI reference.
Superwall for Agents is in private beta and isn't enabled for this app yet
Pushing needs Superwall for Agents enabled on the Superwall app you're pushing to, per app, so a project on several platforms needs it on each one. Nothing in the CLI can turn it on; contact support@superwall.com for access.
Multiple projects found. Pass --project <id>.
Your account has several Superwall projects, and the command can't guess which one you mean. Add --project <id> (and usually --app <id>) to the command.
Diagnostics block the push
Publishing is immutable, so a paywall with diagnostics, a stray non-page file in app/, a duplicate route, refuses to push. The message names each file and where it belongs. These are the same warnings superwall dev prints, so you'll usually have seen them before push time.
A directory missing app/index.tsx or config.ts is the exception: it isn't a paywall yet, so push skips it silently instead of failing. If a paywall you expected didn't ship, check that both files exist. superwall dev lists what it found.
Rename ambiguity in CI
A renamed paywall directory can't be resolved interactively in CI, so the push stops rather than creating a duplicate. Add the --rename old=new flag the error prints. See Push, promote & publish.
x doesn't say which platforms it's for
The project pushes to several platforms, and this paywall's config.ts has no platforms field. Add platforms: ["ios", "android"] (or whichever apply). A push never guesses which app a paywall belongs to. See Configuration.
`annual` has no product id for web
The paywall ships to a platform (here web) that its per-platform product slot doesn't name. Add an id for that platform (annual: { ios: "…", android: "…", web: "…" }) or remove the platform from platforms. See Products.
Several iOS apps in your account / no Web app to bind
A config.ts names a platform superwall.lock doesn't bind yet, and your account has either several apps of that platform or none. Run superwall push interactively once to choose, add "web": "<app id>" under apps in superwall.lock yourself (superwall apps list --json shows ids), or create the app (superwall apps create --platform web). See Several platforms.
This project has no app for "…"
--platform takes ios, android, or web: one the project already binds in superwall.lock (or that app's id); the message lists the ones it has.
paywall x has never been pushed (promote)
Promote only moves the live pointer between pushed versions. There's nothing to point at yet. Push first.
superwall publish requires git
The source snapshot is part of every push, so push and publish both need git, the message names publish whichever you ran. Install git.
Pushing paywalls needs a Superwall account
Run superwall login once interactively, or set SUPERWALL_API_KEY (an sk_… key) in CI. superwall dev needs no login.
Runtime surprises
Prices are example values in dev
The studio reads products from your dashboard when superwall dev is logged in. A slot the dashboard has no product for gets no variables, so prices render as whatever your own fallback shows — the studio will not stand in an invented price. Run superwall login if you haven't, then create the product on the dashboard or repoint the slot; push refuses until you do. Every read stays guarded and the unpriced state stays designed, because the SDK can still deliver a product before its data. The reading rules are in Products.
A number comparison works in dev but not on device
Numeric-looking variables are strings on a real device ("59.99", "7"), while a dashboard sample in the studio may carry numbers. A typeof x === "number" check silently fails on every phone. Coerce with Number() before arithmetic or comparison (Products).
My entry animation already finished when the paywall appears
The SDK preloads paywalls hidden, so components mount long before anyone is looking, a mount-timed animation plays to an empty room. Gate entry animations on presentation, not mount. See Lifecycle & events.
Dark mode looks right on my machine, wrong on device
The mechanism is the dark class the framework maintains on <html>, not prefers-color-scheme. A media query can't see what the device reports and ignores the studio's theme toggle. Style off the class, as shown in Styling.
My fetch to my own API never arrives
The server never saw it — the browser refused to send it. A published paywall runs under a Content-Security-Policy that permits Superwall and Stripe and nothing else, so a call to your own backend is blocked before any network activity: no DNS lookup, no request, nothing in your access logs. The only sign is a console error, Refused to connect because it violates the document's Content Security Policy, and the fetch promise rejecting with a bare TypeError: Failed to fetch.
Name the host in allowedHosts and push again:
export default definePaywall({
allowedHosts: ["api.example.com"],
});Two things make this hard to spot. A rejected fetch looks exactly like a network failure, so a catch written for offline handling swallows it. And if the call only happens on a page shoppers reach near the end of a flow, nothing fails until then. See Allowed hosts.
My link does nothing
Inside a webview, an <a href> either does nothing or navigates the paywall away from itself. Open links through useActions().openUrl instead. See Actions.
Controls sit in the status bar / under the home indicator
The paywall is inset from the edges by default, so this happens for one of two reasons: insets in config.ts turned that off for the edge in question, or the control is position: fixed and positioned from a bare env(), which is 0 in previews, in Android WebView and in some webview contexts. Fixed chrome positions from var(--sw-safe-area-inset-top) and friends: the safe area where the host reports one, a floor for the device and presentation where it doesn't. Chrome inside the layout should not be fixed at all. See Insets.
The payment sheet doesn't open in dev
By design. A preview has no API key, so a Stripe product resolves through the studio's purchase prompt instead of the payment sheet, which lets you test the flow and copy without creating checkout sessions. Push and open the live URL to verify the checkout itself. See Web checkout.
How is this guide?