Architecture
Webhook Pipeline
HMAC verification, ingestion, dispatch, retry, and dead-lettering.
Thirteen POST endpoints live under /webhooks/shopify/*: products create/update/delete, orders create/updated/cancelled, customers create/update, inventory update, product listings add/remove, collections create/update.
Rules that keep it safe
- One dispatcher.
dispatchShopifyWebhookis the single topic→mutation switch, shared byte-for-byte by the live path and the retry path. Past drift between two parallel switches corrupted product data and stored customer IDs in different formats on each path. - Fail loudly. Success → 200. Internal failure → 500, so Shopify retries. A 2xx on failure loses the event.
- Payloads live in their own table.
webhookEventsholds apayloadId;webhookPayloadsholds the JSON (16 MB query-limit lesson). - Pre-filter unknown products. Product/inventory events for products outside the mirror are acknowledged and dropped (the nightly sync would catch them anyway).
product_listings/addis never filtered — it's how new products enter. - OCC-aware retry cron. The stuck-
pendingscan (>10 min) is bounded by acreatedAtwindow so it never reads the range live inserts are writing — the unbounded version made every incoming webhook an OCC conflict with the cron.
Retry and cleanup
- Retry cron every 5 minutes: stuck pending recovered, failed events retried with
2^retryCountminute backoff up to 5 attempts, then dead-letter. - Cleanup cron (03:00 UTC): processed/pending events older than 30 days deleted with their payload rows, bounded batches.
Registering webhooks
The dashboard Webhooks page manages the required topics against the current deployment URL. Deploy the backend first, then register topics — registrations pointing at a not-yet-deployed route get dropped by Shopify after repeated 404s.