Skip to main content

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.ts
  • interfaces/ecommerce.controller.ts
  • infrastructure/ecommerce-provider.factory.ts
  • infrastructure/shopify/shopify.adapter.ts
  • infrastructure/shopify/shopify-webhook.controller.ts
  • infrastructure/ecommerce-polling.processor.ts
  • infrastructure/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

SymptomLikely layer
OAuth URL returns Unknown e-commerce providerEcommerceProviderFactory provider map
OAuth callback redirects with ?error=EcommerceService.handleOAuthCallback or ShopifyAdapter.exchangeCodeForToken
Connection succeeds but webhooks do not arriveShopify webhook registration or public callback URL
Webhook returns 401 Raw body unavailableExpress raw-body route setup for /ecommerce/webhooks/shopify
Webhook returns 401 Webhook signature verification failedShopify 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 connectionMissing/inactive connection record
Product sync is partialOne or more product/collection adapter calls failed inside batch push
Polling does not ingest ordersBullMQ repeat job missing, connection inactive, or Shopify token/scopes invalid
Duplicate storefront order ignoredDedup guard in insertIfNew already saw the external order ID

2) Connection and OAuth checks

Expected connection flow:

  1. Authenticated user calls GET /ecommerce/oauth/url.
  2. Shopify redirects to public GET /ecommerce/oauth/callback.
  3. EcommerceService exchanges the code, registers webhooks, encrypts credentials, stores the connection, and registers polling.
  4. Controller redirects the user to the PWA ecommerce settings page.

When the callback fails:

  1. Confirm the provider query sent to /ecommerce/oauth/url is shopify.
  2. Confirm shopDomain is the merchant's *.myshopify.com domain.
  3. Inspect the ?error= query on the PWA redirect; it is the thrown message from callback handling.
  4. If token exchange fails, check Shopify response status and body in backend logs from ShopifyAdapter.exchangeCodeForToken.
  5. 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.
  • disconnect is 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-sha256
  • x-shopify-shop-domain
  • x-shopify-topic

Processing order:

  1. ShopifyWebhookController requires req.body to be a Buffer.
  2. EcommerceService.handleWebhookEvent finds the connection by shop domain.
  3. Stored webhook secret is decrypted.
  4. Adapter verifies HMAC with constant-time comparison.
  5. Adapter parses supported topics; currently orders/create produces an order.
  6. ingestOrder applies 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:

StatusMeaning
successNo failed items in the batch
partialAt least one item succeeded and at least one failed
errorNo 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:

  1. Confirm the business has an active connection.
  2. Inspect /ecommerce/sync/log?businessId=<id>&type=manual_push.
  3. Use GET /ecommerce/debug/scopes?businessId=<id> to compare granted Shopify scopes against the required sync behavior.
  4. Confirm products have storefront-safe values: name, price, SKU/variant data where relevant.
  5. For inventory-only failures, verify a product mapping exists; syncInventoryLevel skips 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:

  1. Skip if connection is missing or not active.
  2. Pull Shopify orders since lastSyncedAt ?? createdAt.
  3. Ingest each order using the same dedup path as webhooks.
  4. Try to acknowledge each order by tagging it flowpos-ingested.
  5. Update lastSyncedAt after 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:

  1. Re-run manual product or collection sync and inspect the sync log.
  2. Trigger a one-off poll for missed orders.
  3. Reconnect Shopify if debug/scopes shows missing grants or token access errors.
  4. Disconnect and reconnect only when credential repair is needed; the current flow removes the connection record and repeat polling job.
  5. 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/poll is 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.