Naviga order middleware: seven sync flows, no webhooks

A Catholic publisher operating in the United States and Canada

  • Publishing
  • US and Canada
  • Integration, live since 2026
Sync flows in production
7
Source properties gap-checked
79
Properties that mapped directly
49
Order scan window per run
24 hours
Scheduled run cadence
Hourly

Books, subscriptions and pricing live in Naviga. The sales desk lives in HubSpot. Naviga publishes no webhooks for this scope and its customer records carry no modification date, so nothing here could be event-driven. We built a middleware service that runs seven sync flows on a schedule, moves orders, line items, subscriptions, products, companies and contacts in both directions, and keeps a checkpoint, a run record and a replay queue for every flow. It is live.

01 The Challenge

HubSpot is the orchestration and visibility layer in this design, not the system of record. Reps capture order intent in the portal, the backend confirms it, and the portal shows the result. Naviga stays authoritative for books, subscriptions, pricing and confirmed orders. That split only works if the portal is kept current, and the backend gave us little to work with.

Discovery tested every endpoint the design depended on against live responses rather than against documentation. Three answers shaped everything after it. There are no webhooks for this integration scope, so the middleware has to ask rather than be told. Customer records return a create date and no modification date, so a polling job has no way to request the customers that changed. Subscription reads are per customer with no date filter, and the order list endpoints were not usable in that environment at the time, so order discovery could not be assumed either.

One more constraint came from the data. Duplicate email addresses are normal in this customer base, and HubSpot allows one contact per address, so the sync had to carry the duplicates rather than reject them.

02 The Solution

All seven flows run through one pipeline, in the same six stages: trigger, acquire the flow lock, read the source, normalize and match, write, persist the checkpoint. A flow that is already running exits as skipped instead of racing itself, and every run ends in one of five states an operator can read.

Customer discovery walks forward through backend customer identifiers from a saved checkpoint and stops after a long run of consecutive misses, because there is no changed-since call to make. It stores a snapshot and a payload hash per customer. A second flow runs continuously over those snapshots, re-fetches a customer, compares the hash and writes only when the payload actually changed. That pair replaces the delta query the API does not offer. Duplicate addresses survive as plus-addressed variants rather than being dropped.

Products and orders both run from a watermark. The order flow caps each run at a 24 hour changed window, so a long outage cannot become an unbounded batch. It dedupes the summaries, hydrates each order, upserts orders, line items and subscriptions, then runs associations as a separate pass. When every hydration in a run fails, the checkpoint holds rather than advancing, so no window is skipped quietly. Associations that do not resolve go to a backlog table that a repair flow retries.

Two flows run the other way. One searches HubSpot deals sitting at a validation stage, asks the backend to preflight the order, writes the returned pricing onto the deal and its line items, and moves the stage. A later pass creates the order in the backend, writes the backend order number onto the deal, and puts any backend error on a property instead of in a log. The second outbound flow sends companies and contacts changed since the last watermark back as accounts.

03 The Outcome

Orders, line items and subscriptions land in the portal on the hour with their associations already wired, and the deal carries the backend’s own validation result rather than a rep’s guess. Because every flow writes a run record and a checkpoint, a bad hour is a replay rather than a re-import: an operator picks the run, queues it, and the flow resumes from its saved watermark.

Most of what keeps it steady sits in the watermark handling. A window with no orders still advances the watermark, so an empty night is not rescanned forever. A window where every detail lookup fails holds the watermark instead. Failed associations queue and are repaired a quarter past the hour rather than vanishing into a log line. Discovery settled 79 properties before the flows were designed: 49 mapped directly, 23 needed a transform or a second call, and 7 exist nowhere in the source, which is now a documented omission rather than a surprise during user acceptance testing.

How the lifecycle runs now

  1. Sales rep in HubSpot

    1. Rep captures order intent on a deal Next: Deal sits at a validation stage.
    2. In HubSpot: Deal sits at a validation stage Next: Naviga preflights the order, in Pricing preflight.
  2. Outbound flow, read from HubSpot

    1. Naviga preflights the order Next: Pricing written to the deal and its line items.
    2. In HubSpot: Pricing written to the deal and its line items Next: Middleware moves the deal stage.
    3. Middleware moves the deal stage Next: Order created in Naviga, in Order booked.
  3. Outbound flow, a later pass

    1. Order created in Naviga Next: Naviga accepts the order?.
    2. Decision: Naviga accepts the order? Yes: Backend order number written to the deal. No: Backend error written to a deal property.
    3. In HubSpot: Backend order number written to the deal
    4. In HubSpot: Backend error written to a deal property
  4. Order flow, from a saved watermark

    1. Source: Orders changed in Naviga Next: Reads a 24 hour changed window.
    2. In Naviga: Reads a 24 hour changed window Next: Summaries deduped, each order hydrated.
    3. Summaries deduped, each order hydrated Next: Every hydration in the run failed?.
    4. Decision: Every hydration in the run failed? No: Orders, line items and subscriptions upserted. Yes: Checkpoint holds, no window skipped.
    5. In HubSpot: Orders, line items and subscriptions upserted Next: Associations run as a separate pass, in Wired in HubSpot.
    6. Checkpoint holds, no window skipped
  5. Associations pass and repair flow

    1. In HubSpot: Associations run as a separate pass Next: Association resolves?.
    2. Decision: Association resolves? No: Queued in the backlog table. Yes: Order lands with its associations wired.
    3. Queued in the backlog table Next: Repair flow retries a quarter past the hour.
    4. Repair flow retries a quarter past the hour Next: Order lands with its associations wired.
    5. Outcome, done: Order lands with its associations wired
The map follows one order from a rep's intent on a deal through Naviga's preflight and booking. A separate hourly flow brings Naviga's orders into HubSpot with their associations wired.

04 Stack

One deployable service on Railway: Node, Fastify, and Prisma over PostgreSQL. Six flows run on cron, five at the top of the hour and association repair a quarter past, and the seventh runs as a self-scheduling loop. HubSpot calls are pinned to API version 2026-03.

Every outbound call to either system passes through a limiter with three priority classes, and the backend limiter adds a per-flow concurrency cap so one flow cannot starve the others. PostgreSQL holds five tables: runs, checkpoints, replays, customer snapshots and the association backlog. Operations is an HTTP surface rather than a console: health, a manual trigger for one flow or all of them, replay with an optional immediate run, run history, and a state endpoint that returns locks, limiter statistics and recent replays.

05 Architecture and data model

The scheduler starts every flow. Four flows read the backend and write the portal, two read the portal and write the backend, and one drains a backlog table. Every run writes its status and its checkpoint to PostgreSQL, and the ops API can trigger or replay any of them. Nothing enters the service as an event of its own: both directions are discovered by a query rather than announced by one.

Source objectHubSpot objectDirectionMatch key
CustomerCompany and contactTo HubSpotBackend customer id in a custom property
Customer changeCompany and contactTo HubSpotSame id, a payload hash decides the write
ProductProductTo HubSpotBackend product id
OrderOrder, a custom objectTo HubSpotExternal order id
Order lineLine itemTo HubSpotLine item id
SubscriptionSubscription, a custom objectTo HubSpotBackend subscription id
Deal at a validation stageBackend book orderTo sourceBackend order number written back
Company and contact editsBackend accountTo sourceBackend customer id

Every key in that table is a backend identifier held in a custom HubSpot property, never a name and never an email address. Names change when someone tidies a record, and one address can belong to several people in this customer base, so both make unreliable keys. Orders, line items and subscriptions are custom objects rather than deals, because a deal is a pipeline record that a rep moves and these are backend facts a rep should read. The one key that is not a lookup is the line item id: a line arriving without one fails the run rather than landing as half a record.

Ten rows of the property mapping, field names only:

Source fieldHubSpot propertyNote
CustomerIDuniversal_idUpsert key, held as a string
CompanyName, or FirstName and LastNamenameThe account type decides which
PreferredAddress.CitycityPreferred address, not the first one
PreferredAddress.RegionIDstateRegion code
PreferredAddress.PostCodezip
CanPlaceOrder with OrderCreditStatuscustomer_statusDerived, no single status field exists
CustomerDemographicAnswersdiocese_codeFilter the answers by question id
OrderIDhs_external_order_idUpsert key for the order
LineItemIDline_item_idUpsert key for the line, a missing id fails the run
SubscriptionIDnaviga_subscription_idUpsert key for the subscription
A scheduler starts each of seven flows in a single middleware service. Four flows read Naviga and write companies, contacts, products, orders, line items and subscriptions into HubSpot. Two read HubSpot and write orders and accounts back to Naviga. One drains an association backlog. Every run writes its status and checkpoint to PostgreSQL, and an ops API can trigger or replay any flow.
Hosting
Railway
Runtime
Node and Fastify, Prisma over PostgreSQL
Cadence
Hourly cron for five flows, a quarter past the hour for association repair, and a continuous loop for customer updates
Pattern
Polling on a watermark
HubSpot
Sales Hub Professional, Marketing Hub Enterprise, Service Hub Professional
Fields mapped
79

Seventy-nine properties checked against live API responses across five objects, each keyed on the backend identifier rather than on a name or an email address.

What we would do again, and differently

What we would do again

  • Give every flow its own lock, checkpoint and run record from the first commit. It costs a day, and it is the difference between replaying one hour and reloading a quarter.
  • Settle the property gap against live API responses rather than against documentation, before the flows are designed. Two of those answers changed the shape of the sync, and both would have been expensive to meet in build.

What we would do differently

  • Test the discovery endpoints with live calls in week one. The shape of a polling integration rests on how it finds changed records, far more than on the field mapping that usually gets the attention.
  • Add a count reconciliation to the ops surface from the start, so that whether everything landed is a question an endpoint answers rather than one an engineer answers by reading run records.

Systems in this build

Partnerships and accreditations

  • HubSpot Elite Solutions Partner
  • HubSpot Custom Integration Accreditation
  • HubSpot CRM Data Migration Accreditation
  • HubSpot Solutions Architecture Design Accreditation

HubSpot accreditations

  • HubSpot Onboarding Accreditation
  • HubSpot Academy Accreditations

Industry specializations

  • HubSpot Healthcare Industry Specialist
  • HubSpot Financial Services Industry Specialist
  • HubSpot Manufacturing Industry Specialist
  • HubSpot Quote to Cash Capability

Partnerships

  • Google Partner
  • Anthropic Solutions Partner

Have a system in the same shape?

Send the systems, the direction and the record volume. We reply with a scope and an effort band within one business day.

Press Enter or comma to add another system.

Add more detail (optional)

Optional. A sentence or two about the workflow is enough.

We use these answers to scope the work and for nothing else. Privacy policy.

Prefer to talk it through first? Book a technical scoping call

Talk to the people who build these

  • Nijat
  • Sean
  • Prash

Book a scoping call