The Studio

Preview every paywall locally with superwall dev: devices, themes, locales, live variables, and purchase, restore, permission and callback outcomes you choose.

superwall dev hosts the studio at http://localhost:6100: every paywall in your project as a card with a live miniature, and an editor per paywall with a device-frame preview at exact logical size. It's where you check everything you can't check in code.

superwall dev                # the current project
superwall dev examples/*     # several projects at once

dev needs no login, regenerates superwall.d.ts first (so route and product types are always current), and takes --port/-p (default 6100, moving to the next free port) and --host.

Project problems, a stray file in app/, a duplicate route, print as warnings in dev. They're the same checks that block a push, so fix them as they appear rather than discovering them at ship time.

The studio is a host

On a device the native SDK hosts your paywall: it answers the paywall's handshake with the products, the device and user data, and the introductory-offer eligibility, then replies to every purchase, restore, permission and callback request. The studio does exactly the same job, with the same messages in the same order, over the same protocol the visual editor has always used. Your paywall cannot tell the studio from a device, and nothing about previewing is compiled into a published paywall.

The only difference is who decides the outcomes. On a device the store and the OS do. In the studio, you do.

What you can check

  • Devices, iPhone SE through the iPhone 17 line, iPhone Duo closed and open, iPad mini and iPad Pro, plus Pixel, Galaxy and Desktop, and a responsive mode you resize by dragging any edge or corner of the frame (or by typing a width and height), from 200px to 3000px a side; the canvas holds still while you drag. Switching devices also changes what the paywall sees as platform, model, and OS version, so platform-conditional code is testable too, and the insets move with it: a preview reports no env() safe area, so the framework's floors are what position chrome here, keyed on the same attributes a device sets. The iPhone Duo presets report the safe areas measured in Apple's Duo simulator, for each preset and orientation, the way a host reports device.safeAreaInsets. The Android presets reserve the navigation bar below the paywall the way the SDK keeps its webview above it, and the Desktop preset presents itself to a native paywall as a Mac host, which has no safe area at all.
  • The canvas. The stage pans and zooms like a design canvas: scroll to pan, pinch or hold ⌘ while scrolling to zoom around the cursor, drag the empty stage to move, middle-drag anywhere including over a frame, or hold Space for the hand tool. A left drag inside a paywall always belongs to the paywall. Over a local preview the wheel belongs to the paywall whenever the page under the cursor could want it: anything that scrolls or overflows, a handler that consumed the event, or overscroll containment. The canvas pans only when none of that applies; over a published snapshot the browser hands the wheel to the snapshot, so use Space there. The zoom group in the toolbar steps in and out, and clicking the percentage fits everything back into view; ⌘0 fits, ⌘1 is actual size, ⌘+ and ⌘− step. Frames keep their real logical size at every zoom, so a paywall's viewport never changes.
  • Presentation. presentation in config.ts shapes the frame the way the SDK will: a modal rises as a sheet on phones and centres on tablets, a drawer and a popup take the height, width and corner radius you declared, and the paywall's own viewport is that sheet, not the screen behind it.
  • Web paywalls. A paywall whose only platform is web (from platforms in config.ts, or from the app it is bound to in superwall.lock once pushed) is shown as a bare browser viewport, sized to what the browser leaves once its status bar and toolbars are taken, with no island or home indicator, and reports web as its platform. The bars themselves are not drawn: check where they overlap on a real device.
  • Light and dark. The studio's theme toggle changes the interfaceStyle the paywall is told, which drives the same dark class the framework stamps on a device. Check both, always.
  • Locale, switch languages to proof every catalog. The paywall is told a new device locale, exactly as if the phone had changed language. See Localization.
  • Rotation, portrait and landscape, live. See useDevice().orientation in the hooks reference.
  • Introductory offer eligibility. A toggle that flips the store's answer, so both versions of a trial paywall are one click apart. See Free trials.
  • Compare, split the stage between your local build and any pushed version of the paywall (the live one is marked), or another paywall in the project, and both frames scale together as the window changes. Versions come from the dashboard, so this needs a login and a pushed paywall; when they can't be read, the menu says why. A paywall created with superwall create --from also offers Original: the dashboard paywall it was rebuilt from, served as it is today. That pane is view-only, so it shows the editor's own example prices and does not follow the theme, locale or device variables you set on your side. A surface rebuilt from a native screen with superwall migrate --screen offers Original too, as the simulator screenshot captured during the migration. Opening a paywall's studio URL with ?compare=origin lands on that view directly, which is what superwall migrate does when it opens the studio for you. See Migrating from the editor and Migrating native screens.
  • Variables, edit user attributes, device properties, placement params, and per-product variables live in the Variables panel. Values are seeded from your app's real sample data and products when you're logged in; without a login the panel falls back to the same defaults the dashboard shows an app with no traffic. Every edit is sent to the paywall as new variables; nothing reloads. See Variables & personalization.

Outcomes you choose

Purchases, restores, permission prompts and callbacks all resolve from the host. In the studio each one prompts you to pick the outcome, so both branches of every flow are testable. Abandon or fail your own purchase to check those paths; deny your own permission request to check the fallback copy; report nothing to restore. The paywall receives the same messages the SDK would send for that outcome.

Actions the paywall sends to the host (close(), openUrl()) surface as toasts as they happen, so you can confirm one reached the host. Haptics, page views and the purchase request itself are deliberately silent; the prompt covers it.

Products in previews

Product data is read from your dashboard. A slot the dashboard has no product for gets no variables at all — the same thing a device sees when the store has nothing for that identifier — so variables.price is undefined and your own fallback renders. The studio never invents a price, because a confident number for a product it couldn't read is worse than none. Push still refuses a reference the dashboard cannot resolve, see Products.

This is why guarding every read matters: monthly?.variables.price ? `Subscribe · ${monthly.variables.price}` : "Subscribe" renders cleanly whether or not prices resolved. Sign in with superwall login and point the project at a connected app to see real ones.

Dev vs device

superwall devReal device
Product variablesDelivered by the studio from your dashboard; an unresolved slot gets noneDelivered by the SDK from the store
purchase()You pick completed, abandoned or failedReal store
restore()You pick restored or nothing to restoreReal store
close(), openUrl()Toast in the studio (haptics are silent)Acted on by the host
Permissions / callbacksStudio prompts youOS prompt / your app's code
Numeric product variablesWhatever the dashboard sample carriesStrings, Number() before arithmetic
Presentation (paywall_open)ImmediateAfter preload, when actually shown
Web checkout sheetNot mounted: a preview has no API key, so a Stripe product resolves through the studio's purchase promptWorks
presentation, background, featureGating, scrollEnabledRead from config.tsLive on device with iOS SDK ≥ 4.17.0; older SDKs and Android keep the last promoted values until you promote again
onDeviceCacheEnabledIgnored: the preview reloads on every editIgnored in dev mode for the same reason, honoured normally otherwise

Settings on a device in dev mode

superwall dev serves your paywalls to the SDK's dev mode, and the manifest it serves carries the same settings block superwall push stamps, computed once in the framework so the device and the dashboard can't disagree. So changing presentation.style from fullscreen to drawer in config.ts shows up on the next presentation, with no push.

Each setting stands alone: one your config.ts doesn't mention is inherited from the published paywall, so a paywall you've never pushed presents from its own config and a bound one keeps whatever the dashboard configures that config.ts has no word for — intro-offer eligibility, surveys, computed properties, and the loading delay.

Two exceptions, both deliberate. onDeviceCacheEnabled is ignored in dev mode: a dev server reloads the page on every edit, so an enabled cache could serve a stale copy of your local page. notifications always come from your local config.ts, never the dashboard's copy.

An SDK that predates the settings block ignores it and keeps presenting the last promoted version's settings while running your local code, which is worth knowing if a drawer looks fullscreen on device: check the SDK version before checking your config. The Android SDK does not read it yet, tracked with the rest of the Android gaps. gameControllerEnabled is an app-level SDK option rather than a paywall setting, and checkout only steers the web build, so neither changes anything on device.

Preview on a real device

Preview shows a QR code that opens the paywall on a phone, and what it points at depends on where the studio is running.

Locally it carries a superwall_dev link to this machine's dev server, so the phone renders the code you are editing. Four things have to be true, and the SDK checks all of them:

  • your app registers a URL scheme, which superwall dev finds in your Info.plist, app.json or AndroidManifest.xml, or reads from the app's dashboard record
  • the SDK is in dev mode — options.devServer = .url(…) on a physical device, .default only reaches a simulator
  • the build is a sandbox one; a production build refuses and says so
  • the dev server address is loopback, .local, or a private network address

The QR carries the network address superwall dev prints, not the one your browser is on, so scanning works while you browse localhost. Both devices have to be on the same wifi, and iOS asks once for local network permission.

In the dashboard the QR carries a dashboard URL instead, which mints a short-lived token and hands the device to the SDK's debugger — so it shows the live paywall, through the normal paywall pipeline with real products.

App Transport Security blocks plain http, so an app cannot reach a dev server until its Info.plist allows local networking. superwall dev detects this and offers to add the two keys for you. Keep them out of release builds: the SDK refuses a dev server outside a sandbox build anyway, so they buy nothing in production.

Download a screenshot

Download screenshot saves the image Superwall captured of the paywall. Captures run against the published document, so the button stays disabled — and says why — until a version is live. Push alone is not enough; promote is what makes a document a screenshot can be taken of.

You get the 1290 x 2796 render, which is the 6.7"/6.9" App Store screenshot size Apple accepts for a product page and for the attachments a subscription's review needs — so the file uploads to App Store Connect as-is, with no resizing. It is a genuine render at that viewport rather than an upscale of the phone-sized capture, so a paywall that reflows wider is shown reflowing.

In the dashboard

Opening a code-first paywall from the dashboard opens this same studio, hosted in the dashboard, over what Superwall serves: the live snapshot by default, with every pushed version one switch away, and the same devices, themes, locales, variables and outcome prompts as superwall dev. Product, user, and device data come from your app's dashboard records. It is a preview, not an editor: the source stays in your project, and shipping still goes through superwall push, promote, and publish.

A published snapshot carries no preview code. Opened with ?transport=editor it talks to whatever window embeds it, which is how the studio, the dashboard's preview frame and the screenshot service all host it. Opened by an SDK it talks to the SDK.

The Push, Promote, and Publish buttons

The studio has buttons for the same operations as the CLI, good for quick iteration. For actually shipping, prefer the CLI: the buttons skip the diagnostics gate and the dashboard product check, can't resolve renames, and take no -m note. See Push, promote & publish.

How is this guide?

On this page