Just shipped StorePulse — a storefront monitoring + AI-fix app — through
the App Store review process. Sharing what I learned because I couldn’t
find a current write-up before I started.
App Store review surprises (passed first try, but barely):
Protected Customer Data (PCD) approval is a separate gate from app
approval and takes ~10 days. Apply EARLY. We only need name + email
from orders/* webhooks for assignment context — they still made us
submit a written justification per field.
Embedded apps using unstable_newEmbeddedAuthStrategy have a real
quirk where /auth/session-token returns 410 with an empty body,
leaving the iframe blank. The lib expects you to handle this yourself.
Took me 2 days to figure out — wrote a tiny App Bridge bootstrap
that fetches a token and reloads.
Web Pixel + Theme App Extension is a much better combo than ScriptTag
for storefront tracking. Pixel is sandboxed (no theme conflicts), TAE
gives you a stable storefront entry point. Don’t use ScriptTag for
anything new.
Billing API tip: app_subscriptions/update webhook is mandatory if you
support downgrades. Otherwise plan transitions happen on Shopify’s
side and your merchant.plan column stays stuck on “pro” forever
when someone cancels.
Free tier matters more than I expected. Merchants stress-test free
features for weeks before paying. Make the free tier real, not a
crippled 14-day trial.
5 things I’d do differently next time:
Start app store review concurrently with feature work, not after.
Reviewer feedback loops are 3-5 days.
Use Cloudflare Workers + D1 + Durable Objects from day one. The
service-binding pattern between workers is genuinely magical for
apps that need background jobs (theme review, accessibility scans).
Build the embedded /app/billing/confirm route with NO parent layout.
The lib’s /app/* auth wrapper interferes with the post-billing
bounce. Cost me a week of merchants seeing dead-end /auth/login pages.
Decide your free vs paid gating in code BEFORE shipping. Adding plan
gates retroactively to existing routes is painful — pull every check
through one canUseFeature(plan) predicate.
Idempotency on AI/credit-burning actions. Rapid double-click on a
“Generate AI fix” button can charge twice. Add a state guard.
Happy to answer anything about the build. Stack is 100% Cloudflare
(Workers, D1, Durable Objects, Queues) + Anthropic Claude + Remix on
Pages Functions if anyone wants to dig in on a specific piece.
Nice writeup. The PCD timing is the one I see catch most first-time submitters off guard. We applied at the same time as the listing and ended up with the listing approved but PCD pending for a week, which means you can submit but can’t actually onboard real merchants.
One more thing worth adding from our side. The reviewer runs your app in a fresh session with no merchant data prefilled, so any onboarding flow that assumes there are orders, products, or customers in the dev store will silently break the demo. We started seeding our dev store with a script before each submission and that cut review back-and-forth a lot.
Also seconding the session-token 410 thing. The retry-with-fresh-token pattern works but the docs make it sound like a transport error when it’s really an auth lifecycle thing.
How are you handling the App Bridge migration if you scaffolded before they changed the imports?
Appreciate the additional context — all three of these were real for us too.
PCD timing. Same pattern here. We submitted PCD at the same time as the listing thinking they were one process; listing came back approved in 4 days, PCD took 11. Lesson for anyone reading this later: submit PCD first, start the listing review while waiting. The PCD form is a one-pager but its SLA seems decoupled from the listing SLA.
Reviewer dev-store seeding. Yes — we got bitten by this on round one. Our error dashboard rendered empty because the reviewer’s 60-second walkthrough never triggered an actual JS error, and they flagged it as “no data, can’t evaluate.” We ended up shipping a hidden “trigger sample event” button keyed to a dev-store check, but a seed script is cleaner. Curious — what do you seed? We do products + a few orders, but we don’t seed synthetic error events or fake accessibility findings yet, and that’s probably the next thing to add.
Session-token 410. Yeah, the docs framing as a transport error is misleading. It’s really “App Bridge needs a fresh id_token, re-submit.” We ended up replacing the lib’s default 410 handler with our own minimal App Bridge bootstrap HTML at /auth/session-token — fetches window.shopify.idToken() and reloads with the token attached. That killed the iframe-blank failure mode for good.
App Bridge migration. Got lucky there — scaffolded after the imports flipped, so we’ve been on the script-tag + window.shopify global pattern from day one (<script src="cdn.shopify.com/.../app-bridge.js"> + <meta name="shopify-api-key"> in root.tsx, React wrappers like <TitleBar> / <NavMenu> come from @shopify/app-bridge-react but it’s now a thin layer over the CDN script).
If you’re migrating from the old pattern, my read is the bulk of the work is: (1) drop AppBridgeProvider from your root, (2) add the script + meta tags to your document HTML, (3) swap any useAppBridge() consumers for direct window.shopify.* calls. The new pattern is cleaner — no provider context, no hook everywhere, just window.shopify.toast.show() and friends. Worth doing if you’re already in the auth code.