Skip to content

0002. Two-tier order-dispatch pipeline (standard + transformer)

  • Status: Accepted (retroactive)
  • Date: 2026-06-09
  • Deciders: POS Integration team

Context

When a Cata order is paid, it must be delivered to its destination. There are two cases:

  1. Cata-standard receiver — the destination understands Cata's own webhook envelope (HMAC-signed, verbatim order JSON). No per-provider knowledge needed.
  2. Direct-to-POS — the order must be reshaped into a specific POS's create-order request (e.g. Revel's cart/submit), with that POS's own auth.

We wanted both to flow through one pipeline, keep Cata's HMAC integrity exact where it applies, and isolate provider-specific reshaping so it can be generated and reviewed per provider.

Decision

order_dispatch runs as a two-stage JS pipeline:

  1. Standard script (@provider _standard, @topic order_dispatch) — always runs first. It builds the Cata-standard webhook envelope: url = the registered webhook callback (or "" if none), body = null (a sentinel — Go sends the verbatim original payload bytes to preserve byte-for-byte HMAC parity and key order), headers = { "Content-Type": "application/json" }. On this path Go injects X-Cata-Signature / X-Cata-Event / X-Cata-Delivery-ID / X-Cata-Timestamp. The standard script holds no provider knowledge.
  2. Transformer script (@provider <pos-slug>, @topic order_dispatch) — optional. Runs only when the dispatch request supplied builtInTransformer: "<pos-slug>". It receives input.standard (the standard script's output) and fully owns the outbound url / method / headers / body and its own auth. On the transformer path Cata does not add HMAC headers — auth is entirely the script's responsibility.

The dispatch service forwards builtInTransformer into the pipeline; an unknown value surfaces as connectors.ErrUnknownTransformer → HTTP 400. The whole pipeline runs under a single 30-second budget (orders are latency-sensitive). Secrets are never exposed to JS — credentials are looked up server-side from outlet_providers.settings.

Consequences

Positive

  • HMAC integrity is exact on the standard path because Go signs the original bytes, not a JS re-serialization.
  • Provider reshaping is isolated to one transformer script per POS — generatable, reviewable, swappable without touching the standard path.
  • One code path serves both Cata-standard receivers and direct-to-POS targets.

Negative / costs

  • Two execution models for one topic — contributors must understand which path adds HMAC and which doesn't (a common source of confusion; documented in the contract).
  • The transformer owns auth entirely, so a mistake there is a security-relevant bug, not just a mapping bug.
  • The body: null sentinel is subtle: returning an actual object on the standard path would break byte parity.

Alternatives considered

  • Single script does everything. Simpler mental model, but either loses byte-exact HMAC (JS re-serializes) or forces every provider to re-implement the envelope. Rejected.
  • Sign in JS. Would require exposing the webhook secret to the sandbox. Rejected on security grounds.
  • Reshape in Go per provider. Returns us to the compiled-per-provider coupling that ADR 0001 moved away from.

References