0001. JS adapter scripts in centraldb, executed via goja¶
- Status: Accepted (retroactive)
- Date: 2026-06-09
- Deciders: POS Integration team
Context¶
Each POS provider (Revel, Deliverect, Atlas Kitchen, Lightspeed, …) speaks a
different API dialect, yet every integration must perform the same handful of
operations against Cata's Standard API. The legacy kds-management-service
encoded each provider as bespoke, compiled Go packages
(service/RevelService/…, service/DeliverectService/…). That pattern works
but couples every provider change to a Go build-and-deploy of the whole
service, and makes "add a provider" a code-owner task rather than an
integrations task.
We needed a way to add and change provider transforms without recompiling or redeploying the service, and ideally one that an AI agent could generate and a human could review as plain data.
Decision¶
We express each provider integration as a set of JavaScript transform
scripts — one per (provider, topic) — stored as rows in
central.adapter_scripts and executed at runtime by an embedded JS engine.
- Engine:
goja, a pure-Go ES5.1 engine — no CGo, no external process, no Node runtime to operate. - Contract: every script exports
transform(input, context). The Go runtime passes a uniforminputand acontextwith HTTP helpers (get/post/put/delete, plusgetAllfor pagination andgetBatchfor parallel fan-out) andlog. Per-topic input/output shapes are fixed — seeadapter-script-contracts.md. - Topics:
test_connection,list_remote_outlets,sync_outlet,sync_products,order_dispatch,order_status_update,snooze_items(the enum lives indatabase/central_migrations/002-adapter-scripts.sql). - Sharing: scripts are stored in
centraldb, not per-tenant — a Deliverect transform is identical for every tenant on Deliverect. - Lifecycle:
candidate → active → deprecated → archived, with a unique constraint allowing only oneactivescript per(provider, topic). - Source of truth: runtime source lives in Git under
scripts/adapters/<provider>/<topic>.jsand is deployed into the table viascript/deploy-adapter-scripts.sh(seeadapter-scripts-dev-guide.md). - Authorship: scripts are AI-generated, human-reviewed — Claude produces
a candidate, a human approves before it goes
active(see ADR 0005).
Consequences¶
Positive
- Add or fix a provider without recompiling/redeploying the Go service — edit JS, deploy the script row, test via API.
- A transform is reviewable as plain data (diff a candidate against the active version) and generatable by an AI agent.
- One script per
(provider, topic), shared across all tenants — no per-tenant drift.
Negative / costs
- A sandboxed ES5.1 engine: no
async/await, nolet/const, limited stdlib. Scripts must be written to that subset. - Transform logic lives outside the Go type system and tests — correctness leans on the contract docs, sample-payload tests, and human review.
- A 30s execution budget for
order_dispatch/ 10min forsync_productscaps what a script can do; heavy work must usegetAll/getBatchrather than naive loops.
Alternatives considered¶
- Keep compiled Go packages per provider (legacy
kds-management-service). Strong typing and test coverage, but every provider change is a full service build/deploy and a Go-owner task. Rejected for the new service; the legacy service keeps this model. - Node.js / serverless function per provider. Full JS, but adds a runtime to operate, network hops, and cold-start latency on the order-dispatch hot path.
- A declarative mapping DSL instead of code. Cleaner in theory; in practice provider quirks (Revel bundles, fee remainders, dining-option filters) exceed what a simple mapping language expresses.
References¶
pos-adapter-architecture.mdadapter-script-contracts.mdadapter-scripts-dev-guide.mddatabase/central_migrations/002-adapter-scripts.sqlpkg/jsruntime/,internal/service/adapter_execute_service.go