Skip to content

    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-SHA256 signature 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)

    ItemAgreed
    The one bugcustomers/redact deliveries rejected with 401 for some shoppers; seen in the app's logs for 3 development-store test deliveries out of 20
    Expected behaviourEvery 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 touchedapp/routes/webhooks.*.tsx (3 routes), a new app/lib/verify-webhook.server.ts, tests in test/webhooks/
    Access scope, webhook, billing or extension config changesNone. shopify.app.toml is not edited
    Out of scopeThe 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 checksThe 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 a customers/redact payload 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 showed webhook hmac mismatch topic=customers/redact. No live store was used.
    • Root cause: app/routes/webhooks.customers-redact.tsx called await request.json(), then computed the HMAC over JSON.stringify(body). Re-serialising changed the escaped characters, so the digest differed from Shopify's.

    3.2 Change (day 3-4)

    FileChange
    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.tsxUse the helper; respond 401 on a mismatch, 200 after the job is queued
    test/webhooks/*.test.tsNew 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

    CheckResult
    New test on the old codeFails: 401, expected 200
    New test with the changePasses
    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 customerApp 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 scopeApproved; 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 itemStatusEvidence a reviewer would see
    1OAuth runs before anything else on install and reinstallPassFresh 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
    2The embedded app authenticates with session tokensPassauthenticate.admin(request) on every app/routes/app.* loader and action; no auth cookies or local-storage tokens in grep -rn "localStorage|document.cookie" app/
    3The latest App Bridge loads from Shopify's CDN script tagFixapp/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
    4The three mandatory privacy webhooks are subscribed and answeredFixedshopify.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)
    5Every webhook is verified with its HMAC signatureFixedThe 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)
    6All charges go through Shopify's billingNot applicableThe app is free and makes no charges: no billing code or plan in the repository, no charge in the development store's app history
    7The app asks only for the access scopes it usesPassshopify.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
    8Protected customer data access is requested before it is readPassThe 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)
    9New public apps call the GraphQL Admin API, not RESTPassAll 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
    10Storefront features use theme app extensionsPassThe "Notify me" button is an app block in extensions/notify-button/; no Asset API writes and no ScriptTags in the code
    11The app installs and runs without errorsPassInstall, sign-up for an alert, restock and alert email, uninstall: no failed screen and no browser console error on the development store
    12The app does not slow the storefrontPassLighthouse 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
    13The app is served over a valid TLS certificatePassThe app URL and the webhook endpoint both present a valid certificate with no browser or curl -v errors
    14The listing describes only what the app doesFixTwo 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)

    QuestionAnswer
    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)

    1. The new test fails on your old code and passes with the change (npm test -- test/webhooks).
    2. Your existing test suite still passes (npm test).
    3. On your development store, a redact request for a customer with an accented name is answered with 200.
    4. 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

    1. Review and merge pull request #58 in your repository.
    2. Deploy the app server as you normally do.
    3. Run shopify app deploy to release the app version (no config or extension change is in it).
    4. Watch the app's webhook log for a day: customers/redact deliveries 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)

    ItemWhy it mattersSuggested next step
    App Bridge from an old npm package (row 3)Shopify asks App Store apps to load the latest App Bridge from its CDNOne 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 appClient 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 timeMove legacy-sync to GraphQL when convenient