Skip to content

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 uniform input and a context with HTTP helpers (get/post/put/delete, plus getAll for pagination and getBatch for parallel fan-out) and log. Per-topic input/output shapes are fixed — see adapter-script-contracts.md.
  • Topics: test_connection, list_remote_outlets, sync_outlet, sync_products, order_dispatch, order_status_update, snooze_items (the enum lives in database/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 one active script per (provider, topic).
  • Source of truth: runtime source lives in Git under scripts/adapters/<provider>/<topic>.js and is deployed into the table via script/deploy-adapter-scripts.sh (see adapter-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, no let/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 for sync_products caps what a script can do; heavy work must use getAll/getBatch rather 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