Client: International Salon Supplies
Role: Integration Engineer
Period: Nov 2025
Status: Production
Tech stack: Node.js 22, TypeScript, Express, RabbitMQ, Drizzle ORM, SQLite (libSQL), Monday.com GraphQL API, Retail Express REST API, Retail Express SOAP API, fast-xml-parser, Docker, Caddy
The purchasing team tracks imports on a Monday.com board, but purchase orders live in Retail Express. This service keeps both in sync in both directions: ERP changes flow to Monday over RabbitMQ, and edits on the board are written back to the ERP through its legacy SOAP API, with snapshot diffing to prevent sync loops.
The problem
Import purchase orders move through many stages: ordered, sailed, in a container, arrived, received. The purchasing team manages that journey on a Monday.com board, but the purchase orders themselves live in Retail Express (REX). Keeping status, ETA, supplier invoice numbers, sailing dates and container numbers in step between the two was manual double entry.
Architecture
(A) ERP → Monday
ERP change stream → RabbitMQ (durable, prefetch 1)
ORDERS → upsert order snapshot
PURCHASE_ORDERS → only if the PO is tracked on the board:
upsert PO snapshot → Monday GraphQL change_multiple_column_values
ack on success · nack (no requeue) + remote error log on failure
(B) Monday → ERP
Monday webhook (with challenge handshake)
PO number entered → fetch PO from ERP REST → fill 9 columns on the board
ETA / invoice / sailed / container changed
→ load full Monday item → compare with stored snapshot (skip if unchanged)
→ fetch PO + items from ERP → build XML → ERP SOAP CreateUpdatePurchaseOrders
(C) Supplier lookup webhook → page through ERP suppliers → write details back to Monday
(D) Backfill endpoint → cursor-paginated board scan → seed snapshots
Engineering highlights
A hybrid REST + SOAP integration
The ERP's modern REST API is good for reading but can't update the shipping fields on a purchase order. Those writes have to go through the legacy SOAP web service. The service reads over REST with a bearer token, builds nested, escaped XML for CreateUpdatePurchaseOrders, and parses the SOAP envelope to confirm the result. Most real ERP integrations end up with a split like this, and it's better to embrace it than fight it.
Preventing ping-pong
Two-way syncs can loop: A updates B, B's webhook updates A, and so on. Every Monday change is compared against a stored snapshot of that board item. If nothing meaningful changed, the ERP write is skipped. All snapshot tables use idempotent upserts keyed on ERP IDs.
Only sync what matters
ERP purchase-order events are applied only to POs already tracked on the board. A sync flag column is switched off automatically when the ERP marks a PO as received, so completed POs stop generating traffic.
Resilience
- RabbitMQ: 60 s heartbeats, a 30 s connect timeout, exponential-backoff reconnects (5 s doubling, capped at 30 s, up to 10 attempts),
prefetch(1)for ordered processing, and graceful shutdown. - The ERP access token is cached in SQLite with its expiry, so it survives container restarts.
- Errors are sanitised (circular references,
Errorobjects) and shipped to a central error-logging service.
Supplier auto-fill
Typing a supplier code on a second board triggers a webhook that pages through the ERP's supplier list and fills in the supplier's details automatically.
By the numbers
- ~930 lines of TypeScript
- 4 HTTP endpoints plus a queue consumer handling two event types
- 9 mapped Monday columns, 6 SQLite tables
What I'd add next
Verify Monday webhook signatures, and wire up the retry helper for transient ERP failures. Both are small changes that turn a working integration into a hardened one.