Shopify Ecommerce Troubleshooting Runbook
Operational runbook for diagnosing Shopify connection, sync, webhook, and polling failures in apps/backend/src/ecommerce/.
Use this with the source-backed architecture reference in Ecommerce Adapter Architecture.
Scope
Primary codepaths:
application/ecommerce.service.tsinterfaces/ecommerce.controller.tsinfrastructure/ecommerce-provider.factory.tsinfrastructure/shopify/shopify.adapter.tsinfrastructure/shopify/shopify-webhook.controller.tsinfrastructure/ecommerce-polling.processor.tsinfrastructure/ecommerce-connection.repository.ts
Shopify is the only registered provider at runtime. woocommerce is a reserved enum value until a WooCommerce adapter is implemented and registered in EcommerceProviderFactory.
1) Symptom triage
| Symptom | Likely layer |
|---|---|
OAuth URL returns Unknown e-commerce provider | EcommerceProviderFactory provider map |
OAuth callback redirects with ?error= | EcommerceService.handleOAuthCallback or ShopifyAdapter.exchangeCodeForToken |
| Connection succeeds but webhooks do not arrive | Shopify webhook registration or public callback URL |
Webhook returns 401 Raw body unavailable | Express raw-body route setup for /ecommerce/webhooks/shopify |
Webhook returns 401 Webhook signature verification failed | Shopify HMAC secret/header mismatch |
Webhook returns 200 { "ok": false } | App-side processing error after auth; inspect backend logs and sync log |
Manual product sync returns 404 No active ecommerce connection | Missing/inactive connection record |
Product sync is partial | One or more product/collection adapter calls failed inside batch push |
| Polling does not ingest orders | BullMQ repeat job missing, connection inactive, or Shopify token/scopes invalid |
| Duplicate storefront order ignored | Dedup guard in insertIfNew already saw the external order ID |
2) Connection and OAuth checks
Expected connection flow:
- Authenticated user calls
GET /ecommerce/oauth/url. - Shopify redirects to public
GET /ecommerce/oauth/callback. EcommerceServiceexchanges the code, registers webhooks, encrypts credentials, stores the connection, and registers polling.- Controller redirects the user to the PWA ecommerce settings page.
When the callback fails:
- Confirm the
providerquery sent to/ecommerce/oauth/urlisshopify. - Confirm
shopDomainis the merchant's*.myshopify.comdomain. - Inspect the
?error=query on the PWA redirect; it is the thrown message from callback handling. - If token exchange fails, check Shopify response status and body in backend logs from
ShopifyAdapter.exchangeCodeForToken. - If webhook registration fails, the connection can still be created; polling is the fallback path.
Important source behavior:
- Credentials are encrypted before persistence via
CryptoService.multiEncrypt. disconnectis idempotent and returns success even if no connection exists.- Shopify token revocation errors during disconnect are logged and treated as non-fatal.
3) Webhook checks
Webhook endpoint:
POST /ecommerce/webhooks/shopify
Required Shopify headers:
x-shopify-hmac-sha256x-shopify-shop-domainx-shopify-topic
Processing order:
ShopifyWebhookControllerrequiresreq.bodyto be aBuffer.EcommerceService.handleWebhookEventfinds the connection by shop domain.- Stored webhook secret is decrypted.
- Adapter verifies HMAC with constant-time comparison.
- Adapter parses supported topics; currently
orders/createproduces an order. ingestOrderapplies the dedup guard, creates/fetches customer, deducts mapped inventory, and writes a sync log.
Failure behavior:
- Authentication failures throw
401. - Non-auth processing failures are logged and returned as
200 { "ok": false }to avoid provider retry storms. - Unsupported topics are ignored after signature verification.
If every webhook fails with Raw body unavailable, verify backend raw-body middleware for the Shopify webhook route. JSON-parsed bodies cannot be used for HMAC verification because Shopify signs the raw payload bytes.
4) Manual sync checks
Manual product sync:
POST /ecommerce/sync/products?businessId=<businessId>
Manual collection sync:
POST /ecommerce/sync/collections?businessId=<businessId>
The controller returns 202 with a syncLogId, but the current service implementation runs the batch before returning. Use /ecommerce/sync/log to inspect the final status.
Sync log statuses:
| Status | Meaning |
|---|---|
success | No failed items in the batch |
partial | At least one item succeeded and at least one failed |
error | No item succeeded |
Batch behavior from runBatchPush:
- page size is 50
- each item failure is counted and summarized in
errorDetail - product and collection mappings are upserted after successful adapter pushes
When product sync fails:
- Confirm the business has an active connection.
- Inspect
/ecommerce/sync/log?businessId=<id>&type=manual_push. - Use
GET /ecommerce/debug/scopes?businessId=<id>to compare granted Shopify scopes against the required sync behavior. - Confirm products have storefront-safe values: name, price, SKU/variant data where relevant.
- For inventory-only failures, verify a product mapping exists;
syncInventoryLevelskips items with no external variant mapping.
5) Polling fallback checks
Polling queue:
- queue name:
ecommerce-polling - job name:
poll - repeat interval: 15 minutes
- repeat job ID:
ecommerce-poll-{businessId}
Polling is registered after OAuth connection and removed on disconnect. On worker startup, EcommercePollingProcessor deduplicates exact repeatable job keys and leaves one valid repeat job per definition.
To force a poll:
POST /ecommerce/sync/poll?businessId=<businessId>
To run inline for debugging:
GET /ecommerce/debug/poll?businessId=<businessId>
Current debug/poll behavior is diagnostic, not a full production ingestion path: it pulls orders and calls the dedup guard (insertIfNew) to count newly seen external IDs. Use it carefully because it mutates dedup state but does not run the full customer, inventory, and acknowledgement flow used by pollOrdersForBusiness.
Polling behavior:
- Skip if connection is missing or not
active. - Pull Shopify orders since
lastSyncedAt ?? createdAt. - Ingest each order using the same dedup path as webhooks.
- Try to acknowledge each order by tagging it
flowpos-ingested. - Update
lastSyncedAtafter processing the batch.
Acknowledgement failures are logged but do not roll back ingestion. FlowPOS-side deduplication remains the safety net if Shopify returns the order again.
6) Safe recovery actions
Use the least invasive action that matches the symptom:
- Re-run manual product or collection sync and inspect the sync log.
- Trigger a one-off poll for missed orders.
- Reconnect Shopify if
debug/scopesshows missing grants or token access errors. - Disconnect and reconnect only when credential repair is needed; the current flow removes the connection record and repeat polling job.
- If webhooks are broken but polling works, keep the connection active and repair webhook registration/callback routing separately.
Do not manually create ecommerce provider mappings for a new platform unless the adapter is registered in EcommerceProviderFactory.
7) Common pitfalls
- WooCommerce is not available at runtime even if an enum value exists.
- Webhook registration can fail without blocking connection creation.
- Webhook HMAC verification requires the raw request body, not parsed JSON.
- Polling is a fallback, not a substitute for fixing webhook auth/routing.
GET /ecommerce/debug/pollis for development and diagnostics; prefer queued polling for normal operations.- The Shopify adapter uses API version
2024-01; update and test adapter calls before changing it.