AcceptedDecided
ADR 0013: The PDNS client's sanctioned DB bridge (and fixing boundary enforcement)
ADR-0004 makes lib/pdns/ a pure protocol adapter: it speaks the PowerDNS HTTP API and nothing else. RBAC, audit, and DB writes happen in the calling business logic.
On this page
- Status: Accepted
- Date: 2026-05-22
- Deciders: @jseifeddine
Context#
ADR-0004 makes lib/pdns/ a pure protocol adapter: it speaks the PowerDNS HTTP API and
nothing else. RBAC, audit, and DB writes happen in the calling business logic. The boundary was
declared as an ESLint import/no-restricted-paths zone (lib/pdns may not import lib/db,
lib/rbac, lib/audit, lib/auth).
Two things were true and in tension:
The rule wasn't actually enforcing anything.
import/no-restricted-pathsresolves an import to a file path and compares it against the zone'sfrom/targetdirectories - which requires an import resolver. The codebase imports everything via the@/…TS path alias, and no resolver was configured, so the rule never resolved@/lib/dbto./lib/dband never fired. Four files underlib/pdns/had// eslint-disable-next-line import/no-restricted-pathsdirectives that were inert - the boundary, the centerpiece of ADR-0004, was silently unenforced.Four files genuinely need
lib/db. They are bridges by nature, not accidental coupling:registry.ts- builds aPdnsClientfrom apdns_serversrow (decrypts the API key, reads base URL/capabilities). Constructing a client is reading config from the DB.request-log.ts- writes onepdns_requestsaudit row per outbound HTTP call. It is the PDNS transport's own request log; the writer must touch the DB.cluster-picker.ts-choosePeer()reads recentmetric_samplesto apply a latency/zone count routing strategy. The pure decision lives incluster-picker-pure.ts; this file only loads the samples that feed it.sync.ts- cross-server zone-state coordination; enumerates a primary's active secondaries (a DB read) and fans probes across them.
Decision#
Enforce the boundary for real. Replace
import/no-restricted-pathswithno-restricted-importspatterns(matched against the import specifier, e.g.@/lib/db/**), scoped per layer via flat-configfilesoverrides. This works with the@/alias and needs no resolver. The zones from ADR-0004 are preserved:components/**✗ db/pdns/auth,lib/db/**✗ rbac,lib/pdns/**✗ rbac/audit/db/auth.Sanction the four bridge files explicitly. Each carries a file-top
/* eslint-disable no-restricted-imports */with a comment pointing here. The disable is now real (the rule fires) and narrow (only these four files), so the exception is visible in review and greppable, instead of an inert directive hiding an unenforced rule.
Rationale#
- The bridge files are a small, stable, conceptually-coherent set: "turn a DB server/cluster row
into PDNS traffic, and log that traffic back." Forcing the DB reads up into every call site
would duplicate
loadSamples/listActiveSecondariesForPrimaryacross 4–5 callers each - worse coupling, not better. - Making the rule actually fire is the important correction: any new
lib/pdns → lib/dbimport now fails CI unless it's a deliberate, reviewed addition to a bridge file.
Alternatives considered#
- Add
eslint-import-resolver-typescriptsoimport/no-restricted-pathsresolves@/. Rejected for now: a new lint dependency + resolver config to keep a rule thatno-restricted-importsdoes specifier-side with zero deps. - Invert the dependencies (pass samples / secondary lists into
lib/pdnsfrom above). Clean in principle and still the long-term goal forcluster-picker/sync, but it duplicates the DB reads across call sites today and touches load-bearing routing code. Deferred as future work; tracked in the file-top comments.
Consequences#
- The three-layer boundary from ADR-0004 is now genuinely enforced (it previously was not).
- The
lib/pdns → lib/dbcoupling is confined to four named files and documented here. - Future work: relocate
cluster-picker.choosePeerandsync.tsabovelib/pdns(e.g. alib/cluster/domain module) and drop their disables, leavingregistry/request-logas the irreducible bridge.
References#
- ADR-0004 (the three-layer architecture this refines)
eslint.config.mjs(the enforcement)lib/pdns/{registry,request-log,cluster-picker,sync}.ts(the bridge)