Inkress ADRsarchitecture decisions
Overview / Architecture
ADR 0019

The commerce platform is split into services by responsibility and trust zone

AcceptedArchitecture

Context

"Commerce" spans very different concerns: a stateful transactional core (merchants, orders, checkout, the money orchestration), a background-job workload (webhooks, emails, recurring billing), a payment-processing control plane, and the one component that ever touches a cleartext card. These have different failure modes, different scaling and latency needs, different languages that fit them, and — critically — very different PCI trust levels. Putting them in one codebase would drag the whole application into the card-data blast radius and couple unrelated release cycles.

ELIXIR CORETYPESCRIPT · MONEYLEGACYcommerce-apicore + schemacommerce-workerOban jobscommerce-paymentscontrol planecommerce-payments-edgeCDEcommerce-risk · PDPcommerce-adapterssupersededone DB · api migrates
FigureServices split by responsibility and trust zone.

Decision

Split the platform into separate services by responsibility and by trust zone, each its own repo:

  • commerce-api (Elixir/Phoenix) — the transactional core and the public/merchant API: merchants, orders, checkout, discounts, and the orchestration of the money paths. It owns and migrates the MAIN schema (ADR 0016). Never sees a cleartext PAN.
  • commerce-worker (Elixir) — the background-job runtime (Oban): outbound webhooks, emails, subscriptions/billing, appointments. It shares the MAIN database and owns the JOBS database (ADR 0017); commerce-api enqueues into it over HTTP (ADR 0014). Isolating jobs keeps their load and failures off the request path.
  • commerce-payments (TypeScript/Hono) — the PCI control plane: provider adapters (ADR 0008), the append-only ledger and the PanToken vault. Handles PanTokens only, never a cleartext PAN (ADR 0006). Low PCI scope.
  • commerce-payments-edge (TypeScript/Cloudflare) — the CDE: the only place a cleartext PAN exists, and it retains nothing (ADR 0006). Its own repo precisely because it is the highest trust zone — a small, bounded surface tightens change-control and least-privilege for the code that PCI cares about most.
  • commerce-adapters (TypeScript) — the legacy adapter service being superseded by commerce-payments (ADR 0009); it exists during the migration and will be retired.

The organising principle: language fits the job (Elixir for the stateful core and its job runtime; TS/Hono and a Cloudflare Worker for the fast, isolated payment edge), and trust zone dictates the boundary — the sensitive card-handling code is quarantined in the smallest repo that can hold it.

Consequences

  • Positive: the PCI CDE is a tiny, independently change-controlled repo; the rest of the platform is out of the card-data blast radius. Each service deploys and scales on its own cadence.
  • Positive: background-job churn and failures are isolated from the request-serving core; the payment control plane can evolve (and be swapped in behind the compat shim) without touching the transactional API.
  • Neutral / to revisit: the split has real seams to maintain — the shared MAIN database couples api and worker (ADR 0017), the HTTP enqueue contract couples them too (ADR 0014), and the signed checkout-intent canonical couples commerce-api to the edge (ADR 0012). Cross-service contracts must be versioned and kept in step.
  • Neutral / to revisit: two payment services (commerce-payments and commerce-adapters) run in parallel until the migration completes (ADR 0009).

Alternatives considered

  • A single commerce monolith: rejected — it would pull the entire application into PCI scope and the card-data blast radius, and couple unrelated release cycles (a job change forcing an API redeploy, etc.).
  • One payments repo for both the control plane and the CDE: rejected — mixing the PAN-handling edge with the PAN-free control plane enlarges the tightly-controlled surface; keeping the CDE alone in its own repo is the whole point of the trust-zone boundary (ADR 0006/0007).
  • Merging api and worker into one Elixir app: rejected — background-job load and failures would share the request path's runtime; a separate worker isolates the job runtime and lets it be operated and scaled independently.
← 0018 centraprox is the single Caddy front proxy for all *.inkress.com hosts 0020 Tenancy uses polymorphic owner-record tables scoped by a global query hook →