Building a Shopify App in 2026: 8 Lessons I Learned the Hard Way
I’ve shipped two public Shopify apps, Woopify-Sync and Restok, on a stack of Node.js + TypeScript (Express), PostgreSQL, BullMQ with Redis, and a plain JavaScript embedded dashboard. Here are the lessons that cost me the most time, so they don’t cost you the same.
1. Decide custom vs public distribution on day one
A custom app is installed on one store and can’t use Shopify Billing. A public app can be listed on the App Store and charge merchants. You can’t convert a custom app into a public one, so if there’s any chance you’ll list it, create it as public from the start. I had to recreate Restok as a new app and reinstall it everywhere.
2. Check your webhook route paths twice
My restock emails silently never sent. The logs showed 401s on webhook calls, which looked exactly like an HMAC verification failure. The real cause was a doubled path prefix:
app.use('/webhooks', express.raw({ type: '*/*' }), webhooksRouter)
webhooksRouter.post('/webhooks/inventory', handler) // ❌ → /webhooks/webhooks/inventory
webhooksRouter.post('/inventory', handler) // ✅ → /webhooks/inventoryShopify’s requests fell through to an authenticated API router, which returned 401. Route paths are relative to where the router is mounted. And remember webhooks need the raw body for HMAC verification, so mount the raw body parser before any JSON parser.
3. Use App Bridge session tokens, not cookies
Embedded apps run inside an iframe in the Shopify admin, where third-party cookies are unreliable. Get a session token from App Bridge (await shopify.idToken()), send it as a Bearer header, and verify the JWT on your server with your app secret. Also, don’t block your dashboard on App Bridge being ready within a fixed timeout. Render right away and let the token request wait.
4. Turn off ETags for your API inside the admin iframe
Express adds ETags by default. Inside the admin iframe I got 304 Not Modified responses with stale or empty bodies, and the dashboard showed old data. The fix: app.set('etag', false) and Cache-Control: no-store on API and auth routes.
5. Native dialogs don’t work in the embedded admin
confirm() and alert() are blocked inside the Shopify admin iframe. They don’t throw an error; they just do nothing. My “Delete” and “Re-notify” buttons silently failed. Use in-page confirmation (a two-click confirm or a modal) instead.
6. Don’t rely on the hidden attribute when a class sets display
A full-screen “Connect your store” overlay kept covering a perfectly working dashboard. It used the hidden attribute, but a class with display: flex overrode it. One line fixed it: [hidden] { display: none !important; }. When debugging embedded apps, a small on-screen debug panel showing token and API status is often faster than reading logs.
7. Separate billing test mode from NODE_ENV
Development stores can only accept test charges. I first tied test billing to NODE_ENV, and flipping it broke unrelated things like database SSL and caching. Give billing its own environment variable (for example SHOPIFY_BILLING_TEST), and always check the subscription response for errors, not just for a confirmation URL.
8. Prepare for App Store review early
- Implement the mandatory privacy (GDPR) webhooks:
customers/data_request,customers/redactandshop/redact, all HMAC-verified. - Request protected customer data access if you store shopper emails or read orders.
- Pass the automated checks: install and auth flow, redirect into the app UI, TLS, and the latest App Bridge.
- Have a 1200×1200 app icon, screenshots, a privacy policy URL and clear test instructions ready.
- Keep heavy work (emails, imports) in a background queue so webhook responses stay fast. BullMQ with Redis works well for this.
Wrapping up
Most of these bugs looked like something else, such as an auth failure, a caching issue or a broken API. The common thread: when an embedded app misbehaves, check routing, caching and the iframe environment before you rewrite your auth.