SAMPLE: a fictional business. Not a client of Adwaizer.
Shopify app fix: review-readiness report and fix summary for Tidewell Restock Alerts (sample)
Prepared by: Adwaizer AI Consulting Inc. · Engagement: Shopify app bug fix or small feature, Starter tier (one fix in one app, 6 working days) · Report version: sample, October 2026
What this page is. This is the handover report a client keeps at the end of the Shopify app fix. Tidewell Restock Alerts, its developer Tidewell Apps and every person, store, repository, commit and number below are invented to show the format and depth; none of them exist. In a real engagement every row points to the client's own repository, pull request and development store.
How to read the evidence. "Evidence" is what a reviewer can open and check: a file and line in the repository, a test name, a command and its output, or a screen on the development store. Nothing here was run against a real app.
1. Summary
Tidewell Restock Alerts is a fictional public Shopify app that emails shoppers when a sold-out product variant is back in stock. Its developer asked us to fix one bug: some deliveries of the mandatory customers/redact privacy webhook were rejected with HTTP 401, so a shopper's erasure request could go unanswered.
- The bug: the webhook handler checked the
X-Shopify-Hmac-SHA256signature against the JSON body after parsing and re-serialising it, not against the raw request body. Any payload whose bytes changed on re-serialising (non-ASCII names, escaped characters, different key spacing) failed the check. - The fix: the webhook routes now read the raw body bytes, verify the signature on those bytes with a constant-time comparison, and parse JSON only after the check passes. One shared helper replaces three copies of the check.
- The test: a new test signs a fixture payload containing a non-ASCII customer name over its raw bytes. It failed on the old code (401) and passes with the change (200). The existing suite of 212 tests still passes.
- Review readiness: of the 14 items on our Shopify app review checklist, 10 pass, 1 was fixed by this change, 2 need a fix outside this scope and 1 does not apply (section 4).
- Shopify impact: no access scope, webhook subscription, billing or extension config changed, so no merchant re-consent is expected. The client releases a new app version; a new App Store review is not expected for a server-side fix, but Shopify decides that, not us.
2. Written scope (day 1, agreed in writing before work)
| Item | Agreed |
|---|---|
| The one bug | customers/redact deliveries rejected with 401 for some shoppers; seen in the app's logs for 3 development-store test deliveries out of 20 |
| Expected behaviour | Every genuine Shopify webhook delivery passes the HMAC check and is answered with a 200-series status; a delivery with a wrong signature is rejected with 401 |
| Files touched | app/routes/webhooks.*.tsx (3 routes), a new app/lib/verify-webhook.server.ts, tests in test/webhooks/ |
| Access scope, webhook, billing or extension config changes | None. shopify.app.toml is not edited |
| Out of scope | The App Bridge loading change and the listing screenshots found in the review check (section 4); the two REST Admin API calls; any change to the live store |
| Acceptance checks | The four checks in section 6 |
3. The fix
3.1 Reproduced bug (day 2)
- Failing test:
test/webhooks/customers-redact.test.ts› "accepts a genuine delivery whose payload has non-ASCII characters". The fixture is acustomers/redactpayload for a test customer named "Zoë Ménard" (fictional), signed with the development app's test secret over its raw bytes. - Result on the old code: expected status 200, received 401.
- On the development store
tidewell-dev.myshopify.com(fictional, test data only): a test customer with an accented name was created and a redact request was triggered from the store admin; the app log showedwebhook hmac mismatch topic=customers/redact. No live store was used. - Root cause:
app/routes/webhooks.customers-redact.tsxcalledawait request.json(), then computed the HMAC overJSON.stringify(body). Re-serialising changed the escaped characters, so the digest differed from Shopify's.
3.2 Change (day 3-4)
| File | Change |
|---|---|
app/lib/verify-webhook.server.ts (new) | Reads request.arrayBuffer(), computes the HMAC-SHA256 with the app secret, compares it to the header with crypto.timingSafeEqual, returns the parsed payload only on a match |
app/routes/webhooks.customers-redact.tsx, webhooks.customers-data-request.tsx, webhooks.shop-redact.tsx | Use the helper; respond 401 on a mismatch, 200 after the job is queued |
test/webhooks/*.test.ts | New test from 3.1, plus one test per route that a wrong signature is rejected with 401 |
Pull request: tidewell-apps/restock-alerts#58 (fictional). Diff: +74 / -41 lines in 6 files.
3.3 Checks run on the change
| Check | Result |
|---|---|
| New test on the old code | Fails: 401, expected 200 |
| New test with the change | Passes |
Full suite (npm test) | 215 passed, 0 failed (212 existing + 3 new) |
Lint and typecheck (npm run lint, npx tsc --noEmit) | 0 errors |
| Development store: redact request for the accented test customer | App log shows customers/redact accepted, response 200 |
| Development store: delivery with a tampered body (sent from a local script) | Rejected with 401 |
| Separate reviewer session against the written scope | Approved; no file outside the scope changed |
Secret scan of the commits (gitleaks) | No leaks found |
4. Review-readiness table
Every item on our free checklist (adwaizer.com/shopify-app-review-checklist), checked on the development store and in the repository at the fixed commit. Status is one of Pass, Fixed (by this change), Fix (found, outside this scope) or Not applicable.
| # | Checklist item | Status | Evidence a reviewer would see |
|---|---|---|---|
| 1 | OAuth runs before anything else on install and reinstall | Pass | Fresh install and an uninstall-reinstall on the development store both open the permission screen first and land in the app home after accepting; app/shopify.server.ts uses the framework's auth for every admin route |
| 2 | The embedded app authenticates with session tokens | Pass | authenticate.admin(request) on every app/routes/app.* loader and action; no auth cookies or local-storage tokens in grep -rn "localStorage|document.cookie" app/ |
| 3 | The latest App Bridge loads from Shopify's CDN script tag | Fix | app/root.tsx loads App Bridge from an old npm package version, not the CDN script tag as the first script. Outside this scope; quoted as a separate item |
| 4 | The three mandatory privacy webhooks are subscribed and answered | Fixed | shopify.app.toml declares customers/data_request, customers/redact and shop/redact; with the change all three answer 200 to genuine deliveries (tests in test/webhooks/, development-store log in 3.3) |
| 5 | Every webhook is verified with its HMAC signature | Fixed | The shared helper verifies X-Shopify-Hmac-SHA256 against the raw body for every webhook route; a tampered delivery is rejected with 401 (test "rejects a wrong signature" in each route's test file) |
| 6 | All charges go through Shopify's billing | Not applicable | The app is free and makes no charges: no billing code or plan in the repository, no charge in the development store's app history |
| 7 | The app asks only for the access scopes it uses | Pass | shopify.app.toml requests read_products, read_inventory, read_customers; each maps to a feature (variant list, stock level webhook, shopper email on sign-up); unchanged by this fix |
| 8 | Protected customer data access is requested before it is read | Pass | The app stores shopper emails for alerts; the Partner Dashboard shows the protected customer data request approved for email (screenshot from the client, since we had no Partner Dashboard access) |
| 9 | New public apps call the GraphQL Admin API, not REST | Pass | All product and inventory reads use the GraphQL Admin API; two REST calls remain in app/jobs/legacy-sync.server.ts, which only runs for stores installed before the GraphQL move. Noted for the client; outside this scope |
| 10 | Storefront features use theme app extensions | Pass | The "Notify me" button is an app block in extensions/notify-button/; no Asset API writes and no ScriptTags in the code |
| 11 | The app installs and runs without errors | Pass | Install, sign-up for an alert, restock and alert email, uninstall: no failed screen and no browser console error on the development store |
| 12 | The app does not slow the storefront | Pass | Lighthouse performance on the development store's product page (free default theme, mobile): 81 without the app, 78 with the app block, a 3-point drop |
| 13 | The app is served over a valid TLS certificate | Pass | The app URL and the webhook endpoint both present a valid certificate with no browser or curl -v errors |
| 14 | The listing describes only what the app does | Fix | Two listing screenshots show a settings page removed in an earlier version. Outside this scope; the client replaces them before the next listing update |
Shopify updates its requirements; each item on the checklist page links to the Shopify page it comes from. This table does not guarantee approval.
5. Shopify check on the change (day 5)
| Question | Answer |
|---|---|
| New or removed access scope? | No |
| Mandatory privacy webhooks still answered? | Yes, all three, now including payloads that failed before |
| Webhook HMAC and session checks untouched or still tested? | HMAC check rewritten and tested per route; session checks untouched |
| Admin API call removed from a supported API version? | No |
| Billing touched? | No (the app has none) |
| Extension config still deploys? | Yes: shopify app info and config validation on the development app showed no extension or config change |
| App version release or new App Store review? | The client releases a new app version with their usual shopify app deploy. The change is server-side only, so a new App Store review is not expected; Shopify makes that call |
6. Acceptance checklist (for the client)
- The new test fails on your old code and passes with the change (
npm test -- test/webhooks). - Your existing test suite still passes (
npm test). - On your development store, a redact request for a customer with an accented name is answered with 200.
- The Shopify check in section 5 shows no access-scope, webhook, billing or extension change the written scope did not name.
A failed check is fixed within 5 working days at no charge.
7. Release steps you run
- Review and merge pull request #58 in your repository.
- Deploy the app server as you normally do.
- Run
shopify app deployto release the app version (no config or extension change is in it). - Watch the app's webhook log for a day:
customers/redactdeliveries should show 200 only.
We never merge, deploy, release or submit to Shopify for you, and we never touch your live store.
8. Found but not fixed (for a separate scope)
| Item | Why it matters | Suggested next step |
|---|---|---|
| App Bridge from an old npm package (row 3) | Shopify asks App Store apps to load the latest App Bridge from its CDN | One small fix: swap to the CDN script tag and retest the embedded screens |
| Outdated listing screenshots (row 14) | Review checks the listing against the submitted app | Client task: new screenshots from the current settings page |
| Two REST Admin API calls (row 9) | Not a review blocker for this app today; REST may lose features over time | Move legacy-sync to GraphQL when convenient |