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:
- Cata-standard receiver — the destination understands Cata's own webhook envelope (HMAC-signed, verbatim order JSON). No per-provider knowledge needed.
- 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:
- 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 injectsX-Cata-Signature/X-Cata-Event/X-Cata-Delivery-ID/X-Cata-Timestamp. The standard script holds no provider knowledge. - Transformer script (
@provider <pos-slug>,@topic order_dispatch) — optional. Runs only when the dispatch request suppliedbuiltInTransformer: "<pos-slug>". It receivesinput.standard(the standard script's output) and fully owns the outboundurl/method/headers/bodyand 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: nullsentinel 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¶
adapter-script-contracts.md→ Topic:order_dispatchadapters/revel.md→order_dispatchinternal/connectors/dispatch_pipeline.goscripts/adapters/_standard/order_dispatch.js