A Shopify app (Remix) that forwards selected Shopify events to any HTTPS endpoint the merchant owns. Merchants pick their events with checkboxes, paste a destination URL, and every matching store event is relayed as a signed JSON payload — with a delivery log for every attempt.
Shopify store
│ orders/create, products/update, carts/update, …
▼
/webhooks/events (HMAC verified by @shopify/shopify-app-remix)
▼
Dispatcher (signs, POSTs, retries, logs)
▼
Merchant endpoint
| Piece | Choice |
|---|---|
| Framework | Remix 2 (Vite) — Shopify's official app stack |
| Shopify SDK | @shopify/shopify-app-remix v5, Admin API 2026-07 |
| Database | Prisma + SQLite locally (swap for Postgres in prod) |
| Embedded UI | App Bridge CDN + ui-nav-menu (no Polaris) |
| Auth | Token exchange (unstable_newEmbeddedAuthStrategy) |
npm install
npm run setup # prisma generate + migrate deploy
npm run dev # shopify app dev (creates the tunnel, fills in .env)npm run dev asks you to pick/create an app in your Partner account and writes
the credentials for you. For a manual run, copy .env.example to .env and
fill in SHOPIFY_API_KEY, SHOPIFY_API_SECRET, SHOPIFY_APP_URL.
Link this folder to an existing app instead:
npm run config:link # shopify app config link
npm run deploy # shopify app deploy| Route | Purpose |
|---|---|
app._index |
Overview — stats, recent deliveries, setup checklist, test send |
app.settings |
Destination URL + event checkboxes; saving syncs Shopify |
app.logs |
Full delivery history with delivered/failed filters |
Event webhooks are per shop, created from the settings page rather than
declared in shopify.app.toml, because each merchant picks a different set.
On save, syncSubscriptions() (app/lib/settings.server.js) reads the shop's
current subscriptions and reconciles them:
- ticked but missing →
webhookSubscriptionCreate - ticked but pointing at a stale callback URL →
webhookSubscriptionUpdate(this is what keeps a rotatingshopify app devtunnel working) - unticked →
webhookSubscriptionDelete
Only lifecycle webhooks (app/uninstalled, app/scopes_update) live in
shopify.app.toml.
| Group | Events |
|---|---|
| Orders | orders/create, orders/paid, orders/fulfilled, orders/cancelled |
| Products | products/create, products/update, products/delete |
| Customers | customers/create, customers/update |
| Carts | carts/create, carts/update |
Shopify has no universal add_to_cart webhook — carts/create and
carts/update are the closest server-side signal. True add-to-cart tracking
needs a Theme App Extension (see Roadmap).
Every event lands on the single /webhooks/events endpoint, where the Shopify
SDK verifies the HMAC. The route responds 200 immediately (Shopify times out
at 5s) and hands off to the dispatcher, which:
- skips the event if the shop has no destination URL or the event is unticked
- POSTs a JSON envelope to the merchant's URL (10s timeout)
- retries up to 3 times (1s, then 5s) on network errors, 5xx and 429 — 4xx responses are treated as final and are not retried
- writes a
DeliveryLogrow with status, HTTP code, duration and attempt count
The newest 200 deliveries are kept per shop.
Verify the signature with the per-shop secret shown on the settings page:
const expected =
'sha256=' + crypto.createHmac('sha256', SIGNING_SECRET).update(rawBody).digest('hex')
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedHeader))| Model | Holds |
|---|---|
Session |
Shopify access tokens (managed by the SDK's session storage) |
Shop |
One row per install |
Setting |
Destination URL + per-shop signing secret |
EventSetting |
Which events are on, plus the Shopify subscription id |
DeliveryLog |
Status, HTTP code, response, duration and attempts per event |
- Scopes:
read_orders,read_products,read_customers. Order and customer data is protected customer data — request access in the Partner Dashboard. - Database: SQLite is single-file and local. Point the datasource at
Postgres and re-run
prisma migrate deployfor production. - Retries: they run in the app process. On a serverless host, move the dispatcher onto a real queue so retries survive a cold shutdown.
Shipped (MVP): admin settings, event checkboxes, per-shop subscriptions, signed delivery with retries, delivery logs, test event.
Next: Theme App Extension for true add-to-cart tracking, a durable retry queue, per-event filters, and billing.
The original Vite + React mockup this app's design came from. It is standalone
(its own package.json) and is not part of the build — kept as a visual
reference. npm run dev inside that folder still runs it.