A small Node.js service that listens for Shopify fulfillment webhooks and mirrors “fulfilled” orders into Fishbowl Advanced.
This repo includes a fully runnable local demo (no Shopify store / no Fishbowl instance required) using:
- a Mock Fishbowl HTTP server
- a Mock Shopify mode that returns “FULFILLED” for test orders
When Shopify sends a webhook that an order is fulfilled:
- Receives webhook:
POST /webhooks/shopify - Verifies Shopify HMAC signature (
X-Shopify-Hmac-Sha256) - De-dupes deliveries using
X-Shopify-Event-Idstored in SQLite (webhooks are at-least-once) - Confirms the order is actually FULFILLED (Shopify GraphQL)
- In demo mode we mock this check
- If fulfilled, triggers a Fishbowl action via Import (
POST /api/import/:name) using CSV data - If Fishbowl fails, the bridge does not change Shopify; it logs + optionally emails an alert
- Unlisted YouTube: https://youtu.be/P-oLhBJOCX8
- Node.js 20+ recommended (18+ usually works)
npm installwill build native deps (SQLite), so you’ll want a normal dev environment
npm install
npm run demoWhat you should see:
- Webhook #1 succeeds → Fishbowl “import” called for order
1001 - Webhook #2 is a duplicate → dedupe triggers and Fishbowl is not called again
- Webhook #3 forces Fishbowl failure (
9999) → bridge logs the error and returns 200 with an error field
The demo keeps the mock server running so you can inspect calls:
curl http://127.0.0.1:2456/__mock/requestsYou’ll see arrays like:
logins: Fishbowl login calls (one per non-deduped attempt)imports: CSV payloads your bridge sent to/api/import/:name
Stop the demo servers with Ctrl+C.
npm run dev— run the bridge (reads.envvia dotenv)npm start— run the bridge without nodemonnpm run mock:fishbowl— run Mock Fishbowl onlynpm run send:webhook— send a signed test webhooknpm run demo— starts mock Fishbowl + bridge and runs 3 webhook scenarios
- Start the mock Fishbowl:
npm run mock:fishbowl- Start the bridge with demo env:
# PowerShell:
Copy-Item .env.demo .env
npm run dev- Send a signed webhook:
npm run send:webhook -- --orderNumber 1001Optional flags:
--eventId demo-event-123(controls dedupe key)--topic orders/fulfilled--payloadFile ./payload.json
The demo uses .env.demo (safe to commit). Key values:
SHOPIFY_MODE=mock(no real Shopify API calls)FISHBOWL_BASE_URL=http://127.0.0.1:2456(mock server)FISHBOWL_MOCK_FAIL_ORDER_NUMBERS=9999(forces a failure path)
Copy .env.example to .env and fill in:
SHOPIFY_SHOP_DOMAIN—your-store.myshopify.comSHOPIFY_ACCESS_TOKEN— Admin API token (used for GraphQL fulfillment status check)SHOPIFY_WEBHOOK_SECRET— secret configured for the webhook subscriptionSHOPIFY_MODE=real— enables real Shopify GraphQL check
FISHBOWL_BASE_URL— your Fishbowl server URLFISHBOWL_USERNAME,FISHBOWL_PASSWORD— Fishbowl credentialsFISHBOWL_FULFILLMENT_IMPORT_NAME— name of the Fishbowl Import mapping (spaces typically become-)FISHBOWL_IMPORT_HEADERS— comma-separated CSV headers Fishbowl expectsFISHBOWL_IMPORT_ROW_TEMPLATE— row template using{{placeholders}}
Supported placeholders:
{{orderNumber}}{{trackingNumber}}{{carrier}}{{shipDate}}(YYYY-MM-DD)
Leave blank to disable alerts:
SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASSALERT_TO_EMAIL,ALERT_FROM_EMAIL
Bridge:
GET /health→{ ok: true, version }POST /webhooks/shopify→ main webhook receiver
Mock Fishbowl (demo only):
GET /healthGET /__mock/requests→ view captured requestsPOST /api/loginPOST /api/import/:namePOST /api/logout
Shopify webhooks are at-least-once, meaning the same event may be delivered multiple times.
We store processed event IDs (X-Shopify-Event-Id) in SQLite:
- default location:
./data/idempotency.sqlite(configurable viaDATA_DIR)
Duplicates are acknowledged with HTTP 200 and skipped.
This is intentionally a small “bridge”:
- ✅ HMAC verification
- ✅ basic idempotency / dedupe
- ✅ Fishbowl trigger + failure alerting
- ✅ runnable local demo
Out of scope (by design):
- complex retry queues / job workers
- multi-tenant SaaS auth, dashboards, etc.
src/routes/*— HTTP routes (health + webhook)src/shopify/*— Shopify verification + client (mock/real)src/fishbowl/*— Fishbowl client (login/import/logout)src/store/*— SQLite idempotency storesrc/notify/*— email alertstools/mock-fishbowl.js— local Fishbowl simulatorscripts/*— demo runner + webhook sender
MIT
