Inkress ADRsarchitecture decisions
Overview / Infrastructure
ADR 0017

Commerce Postgres is VPC-only, split into a MAIN and a JOBS database

AcceptedInfrastructure

Context

The commerce services hold the platform's most sensitive data — merchants, orders, payment records, webhook credentials. Exposing that Postgres to the public internet (even password-protected) is a standing risk: it is scannable, brute-forceable, and one leaked credential from reachability. Separately, the platform runs a high-churn background-job workload (Oban) whose write/vacuum pressure should not sit on the same database as transactional order data.

Decision

Run the commerce databases as managed Postgres inside a private VPC, reachable only from within that VPC, and split them into a MAIN database and a separate JOBS database.

  • VPC-only reachability. The databases listen on private VPC addresses; there is no public route to them. The application containers on the prod server sit inside the same VPC and connect over the private network (commerce-api reaches MAIN via DATABASE_URL and jobs via JOBS_DATABASE_URL; commerce-worker uses MAIN_DATABASE_URL/JOBS_DATABASE_URL). A laptop or any host outside the VPC cannot connect at all — which is the point.
  • MAIN vs JOBS separation. The MAIN database holds transactional data (merchants, orders, webhook_urls, notifications, …); the JOBS database holds the Oban job tables. Both commerce-api and commerce-worker connect to both via distinct Ecto repos, but commerce-api is the only migrator of the MAIN schema (ADR 0016). Keeping jobs in their own database isolates job churn from transactional load and lets the two be sized and tuned independently. (A third, legacy VERK_DATABASE_URL database survives from the pre-Oban era and is being retired with the Verk→Oban migration — ADR 0014.)
  • Operator access is deliberately indirect. Because nothing outside the VPC can reach the DBs, diagnostics run by borrowing a running prod container's network namespace over SSH (a throwaway psql client sharing the container's network) — there is no bastion port and no public endpoint to leave open.

Consequences

  • Positive: the primary data store has no public attack surface; reachability requires being inside the VPC, which in practice means being one of our own containers.
  • Positive: background-job write pressure is isolated from order/transaction queries; the JOBS database can be operated separately.
  • Neutral / to revisit: operational access is inconvenient by design — running a one-off query or a migration diagnostic means going through a prod container's network namespace, and there is no quick laptop connection. Stale public host entries in old env files are a trap; the private VPC address is the only working one.
  • Neutral / to revisit: two services share one MAIN database (a shared-database integration between api and worker), so the schema is a coupling point between them even though only api migrates it.

Alternatives considered

  • Publicly-reachable Postgres with firewall/IP allowlisting: rejected — it still presents a public endpoint to scan and one allowlist mistake from exposure; a VPC-only database has no public route to begin with.
  • A single database for both transactional data and jobs: rejected — Oban's high-churn job table would compete with order queries for the same I/O and vacuum budget; separating JOBS keeps that load off the transactional store.
  • A public bastion/jump host for DB access: not adopted — it is a standing exposed surface to maintain and secure; borrowing a prod container's netns for the rare diagnostic avoids keeping any door open.
← 0016 Schema migrations run automatically at deploy, so every migration must be expand/contract 0018 centraprox is the single Caddy front proxy for all *.inkress.com hosts →