Skip to content

ADR 012 Public Config Surface

Daniel Truong edited this page Oct 1, 2026 · 3 revisions

ADR-012: A second config document for the public site

Status

Accepted.

Context

GET /config serves DEMI's own frontend: it reads the config container's one document, overlays it on app settings, and degrades to app settings alone when the document is missing or Cosmos is unreachable. Its OVERRIDABLE_KEYS (ENVIRONMENT, KEYCLOAK_*, BANNER_COLOUR, USE_MOCK_DATA) are DEMI's own settings, not eagle-public's.

eagle-public boots from eagle-api's GET /api/config, backed by eagle-api's own MongoDB Config document. eagle-api has no Config write controller — that document is edited in Mongo by hand — so there is no write to hang a push handler off the way projects and documents push on their own controllers.

Decision

Seed a second item, id: 'public', into the same config container. GET /config/public serves it, unauthenticated like /config. PUT /eagle/config/public writes it, on the authenticated write chain like every other eagle-api push. eagle-api keeps it current: GET /api/config compares the payload it is about to serve against the last one it sent, and calls PUT /eagle/config/public when the two differ (including on the first read after the pod starts). The push is fire-and-forget and retried on a later read if DEMI rejects it. The read path is the only hook available, since Config has no write controller of its own. A devbox script, src/scripts/seed-public-config.js, reads eagle-api's live /api/config and writes the allowlisted keys directly — this is the bootstrap, run before the first push exists to create the document, and the manual override, run to force the copy back to eagle-api's live payload. It is not how the copy stays current day to day.

eagle-api's Mongo Config document stays the source of truth and the kill switch for both ACCESS_GATE and the search backend. This copy follows it automatically; see Seeding for the bootstrap command.

/config/public fails closed, the opposite of /config. A missing or unreadable document answers 503 with Cache-Control: no-store rather than defaulting, because a defaulted payload would serve ACCESS_GATE: false and open eagle-public's access curtain — the wrong direction to fail in. /config degrades to app settings because there both was already a document-free answer, answered a private admin app, and the app settings are DEMI's own real values. Neither is true here: there is no meaningful default ACCESS_GATE, and the caller is the public internet.

/config/public sits outside the ACL and the entity catalog, for the reason /config and apikeys already do: both endpoints are unauthenticated by design, so there is no visibility predicate to compose and no read[] to check. What gates a public, unauthenticated payload is the controller's key allowlist (PUBLIC_KEYS) rather than a permission field — a key reaches an anonymous caller only once it is added to that list, not merely once it is written into the document. An allowlisted key can also be dropped by a shape check (EXTENDED_PROJECT_PAGES today), on the push, the seeder and the GET alike.

PUBLIC_KEYS is its own list, not a slice of OVERRIDABLE_KEYS. The two documents answer different frontends with different key sets; sharing a list would let a key meant for the DEMI admin console become public because the lists were merged.

Consequences

  • eagle-public gains a second config source (CONFIG_PATH, see eagle-public's Configuration Management wiki page) that it tries before falling back to /api/config, never in place of it.
  • The push rides GET /api/config, so this copy only follows changes a request actually surfaces. A pod that never receives a read after a Mongo edit holds a stale hash until one arrives; in practice every environment gets frequent reads from the frontends themselves.
  • The config container holds two items with different failure behaviour on the same read path; a third audience needs its own item and its own allowlist, not a branch on either existing one.
  • No bicep change: the container already partitions by /id, and a second item under an existing partition key needs no schema change.

See also

DEMI Wiki

Home

Current system

Azure

Domain

ADRs

History

Related

Clone this wiki locally