What broke when we moved paywall tests to the web, and how we fixed it?

We moved onboarding and the paywall to the web so we could tweak pricing and copy daily instead of waiting on app reviews. It worked, but a few things broke on day one.

  • CDN cache bled variants. Fix: short TTL on experiment assets, plus a cache-busting version param tied to the experiment id.
  • Users bounced between web and app and got reassigned. Fix: server-side bucketing and we stamped variant_id into the deep link to the app, then wrote it to user defaults on first open.
  • Flicker from client-side A/B scripts. Fix: render variant server-side. No flicker, fewer mismatches.
  • Stripe domain verification and Apple Pay sheet errors. Fix: one verified domain per environment and a preflight check in CI.
  • Sample ratio mismatch when we paused a variant mid-day. Fix: schedule starts at the top of the hour and lock split until the test ends; if you truly must pause, toss the day’s data.

Net, time-to-learn dropped from weeks to a few days. Paid conversion lifted modestly, mostly from faster copy/pricing iterations. If you’ve gone web-first on paywalls, what were your biggest gotchas, and what guardrails kept your price tests honest?

Lock assignment on the server and carry the variant through every hop.
Use a cookie plus a variant param in your deep link.
Short TTL on assets to avoid stale paywall copy.
I switched to server-rendered variants to kill flicker.
Web2Wave handled variant locking for me out of the box, which saved time.

Server-side bucketing and deep link the variant id into the app. No client-side reassign.
Schedule test start and stop windows.
I build and edit paywalls on Web2Wave.com so changes go live fast. That lets me iterate copy mid-day without a new build.

Server-side assignment fixed most issues for me.

Also add the variant id to the app open event so analytics do not drift.

Short TTL on CDN and turn off client-side experiments on paywalls.

Server render variants then lock with deep links

Your fixes are right. I also add two things. A pre-launch smoke test that cycles through each variant via a QA query param and verifies copy, price, and payment methods. And a sample ratio monitor that pings Slack if splits drift beyond 2 percent. If you use wallets, ensure the Apple Pay or Google Pay sheet matches the variant price. I store variant_id in the payment metadata for reconciliation later.

I ran into cache bleed on Cloudflare. Setting a cache key that included experiment_version and a 5 minute TTL fixed it.

For reassignment, I pass variant_id in the deep link and write it to the user profile so it sticks across sessions.

Server-side split and cache control solve most of this.