11 KiB
Phase 24: AWS Route 53 DNS Sync - Context
Gathered: 2026-08-05 Status: Ready for planning
## Phase BoundarySync DNS hosted zones/records from AWS Route 53 into Postgres on a schedule,
support full CRUD back to Route 53 from Pulse for common record types, track
record-level change history over time (both Pulse-initiated and externally
detected drift), log every sync and CRUD operation for audit (including
failures), and integrate into the existing per-system sync section
(scheduler, /admin/sync UI, health checks) alongside Autotask/Datto
RMM/Veeam/PAX8. AWS credentials are resolved via BWS (Bitwarden Secrets
Manager), not plaintext env vars.
CRUD Scope & Guardrails
- D-01: Writable record types are the common set only — A, AAAA, CNAME, MX, TXT, SRV. NS and SOA are excluded from the write path (zone-delegation records; editing them risks breaking the zone).
- D-02: Records only, not zones. Pulse can create/update/delete records within hosted zones that already exist in Route 53. Hosted zone creation/deletion (domain onboarding/decommissioning) stays outside Pulse (AWS console or infra-as-code).
- D-03: Destructive record operations (update/delete) execute immediately — no phishing-style staged/two-step approval gate. Every change is logged with actor/timestamp/before/after so mistakes are traceable after the fact, not blocked beforehand.
- D-04: CRUD is gated at
requireAdmin()(admin + super-admin) — the same bar as other write-capable admin surfaces in Pulse, not a stricter super-admin-only gate.
Change Tracking & Audit Schema
- D-05: Dedicated Route 53 tables, not a reuse of the phishing
pipeline's
audit_eventstable. New migration introducesroute53_zones/route53_records/route53_record_history/route53_audit_log(naming for planner/researcher to finalize) — mirrors how Veeam and Datto RMM each own their tables rather than sharing a cross-domain audit schema. - D-06: Change history is written both for Pulse-initiated CRUD and for
sync-detected drift (a record changed outside Pulse, e.g. directly in the
AWS console). Each history row is tagged with a
sourcefield:pulse_crud|sync_detected_drift, so the query "did someone change this outside Pulse?" is answerable. - D-07: Failed AWS API attempts (rate-limited, invalid record, AWS-side
error) are also logged in the audit trail — attempted before/after +
error message +
status: failed— not just successful writes. - D-08: Retention is unbounded — no purge job. Matches existing Pulse convention; no audit/history table in this codebase currently has an automatic retention/purge mechanism.
Admin UI & Sync Integration
- D-09: New tile on
/admin/sync(same list as Veeam/Datto RMM/PAX8) plus a dedicated/admin/sync/route53detail page for zones, records, and history — the existing per-integration pattern, not folded into an existing page. - D-10:
/admin/integrationsdisable toggle forroute53is display-only (suppresses health-check display; scheduler/sync/CRUD keep working underneath) — the default behavior per CLAUDE.md. Route 53 is not a second PAX8-style exception that blocks sync/writes when disabled. - D-11: Sync cadence is incremental + periodic full — more frequent incremental checks plus a daily full reconciliation, rather than a single daily full sync. Trade-off (more API calls against Route 53 rate limits for better real-time drift detection) accepted knowingly.
- D-12: The health-check row for Route 53 goes beyond the generic
auth-check + last-sync-age pattern used by other integrations — it also
includes a DNS-specific delegation check: compare each hosted zone's
Route-53-authoritative NS records against a live public DNS lookup
(e.g. Node's
dnsmodule or a DoH resolver) for that domain, flagging a mismatch as degraded health. No manually-maintained "expected NS" field — the live lookup is itself the source of truth to diff against.
Claude's Discretion
- Credentials & AWS account scope — not discussed interactively (user
deliberately skipped this topic, treating it as already settled). Codebase
scouting found uncommitted infrastructure already in place:
docker-entrypoint.sh(new, untracked) plus diffs toDockerfileanddocker-compose.ymlthat install thebwsCLI and wrap the app's start command asbws run --project-id "$BWS_PROJECT_ID" -- node server.jswhenBWS_ACCESS_TOKENis set, falling back to a plainnode server.jsotherwise. This means Bitwarden secret injection happens at the container-entrypoint layer, before the Node process starts — the app itself never calls a BWS SDK; AWS credentials simply appear as normalprocess.envvalues by the timegetRoute53Client()-style code runs. Researcher/planner should: (1) follow the exact existinglib/services/<name>-factory.ts+is<Name>Configured()pattern used by every other integration, reading credentials fromprocess.env; (2) confirm the actual env var names with the user (e.g.AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION, vs aROUTE53_*-prefixed variant) before finalizing the factory — this wasn't locked in discussion; (3) add aROUTE53_*(orAWS_*) row to CLAUDE.md's integration env-prefix table once confirmed. - AWS account scope — not discussed. Default assumption for planning purposes is a single AWS account holding all client hosted zones (the common MSP pattern), not per-client AWS accounts/cross-account IAM roles. Flag during research if this assumption looks wrong once the actual AWS setup is inspected.
- Exact record-change diff granularity (whole-recordset replace vs
individual value diffing) — left to researcher/planner, informed by how
the AWS SDK's
ChangeResourceRecordSetsAPI actually models a record update. - Table/column naming inside the dedicated Route 53 schema — D-05 locks
"dedicated tables," not literal names; researcher/planner should follow
existing migration conventions (
snake_case, audit columnscreated_at/updated_at/synced_at/is_deleted/deleted_at).
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
No external specs, ADRs, or docs reference AWS Route 53 anywhere in this repo — ROADMAP.md's Phase 24 section has no "Canonical refs:" field, and no seed file covers this integration. Requirements are fully captured in the decisions above and in ROADMAP.md's Phase 24 Success Criteria.
Closest existing analogs (not canonical docs, but the patterns to follow)
lib/services/veeam-factory.ts,lib/services/veeam-sync-service.ts— factory + sync-service pattern for a full external integration with scheduler + admin UI + health checklib/services/datto-rmm-factory.ts,lib/services/datto-rmm-sync-service.ts— second reference implementation of the same patternlib/services/sync-scheduler.ts—sync_typeunion,ScheduleConfig,defaultSchedulesarray to extend forroute53-incremental/route53-fulllib/services/integration-health.ts— health-check aggregator to extendapp/admin/sync/page.tsx— integration tile list (id/category/product/description/href/logo/color) to extend with aroute53entrymigrations/081_*.sql—integration_settingstable backing the/admin/integrationsdisable toggle (D-10)
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
components/admin/DataTable.tsx— for the zones/records list on the new/admin/sync/route53pagecomponents/admin/DetailModal.tsx— for record detail/history drill-down (formatted/raw tab pattern already established)app/admin/sync/page.tsxtile array — extend with aroute53entry following the exact shape used forveeam/datto-rmm/pax8
Established Patterns
lib/services/<name>-factory.ts+is<Name>Configured()— credential lazy-load + config-check pattern every integration follows; Route 53 client should match this exactly (see Claude's Discretion above)lib/services/<name>-sync-service.ts— incremental/full sync againstlastTrackedModificationDateTime-style cursors, batched viapostgresClient.bulkUpsert()lib/services/sync-scheduler.ts— node-cron singleton; new sync types added to thesync_typestring union anddefaultSchedulesseed arraylib/services/integration-health.ts— per-integration health check, cached ~5 minutes, read by both/admin/integrationsand/admin/syncmigrations/081_*.sqlintegration_settings— disable-toggle table withdisabled_by/disabled_at/disabled_reasonaudit columns; per CLAUDE.md, PAX8 is currently the only integration where disabling also blocks sync/writes — Route 53 explicitly does not join that list (D-10)
Integration Points
lib/services/sync-scheduler.ts— addroute53-incremental/route53-fullto thesync_typeunion anddefaultScheduleslib/services/integration-health.ts— add aroute53health entry (auth check + last-sync age + the D-12 NS-delegation check)app/admin/sync/page.tsx— addroute53tile; newapp/admin/sync/route53/page.tsxdetail page- New
app/api/route53/*routes for zone/record read + CRUD + history - New numbered migration (planner/researcher to confirm the current highest migration number — duplicates exist at 002/004/009, alphabetical apply order per CLAUDE.md) for the dedicated Route 53 tables (D-05)
docker-entrypoint.sh/Dockerfile/docker-compose.yml— already modified (uncommitted) to wire BWS secret injection; see Claude's Discretion above
</code_context>
## Specific Ideas- Health check must include a live public DNS lookup for NS delegation mismatch (D-12) — not a manually-maintained "expected NS" field.
- Audit trail must capture failed AWS API attempts, not just successful writes (D-07).
- Change history must distinguish Pulse-initiated changes from
sync-detected external drift via a
sourcetag (D-06).
None — discussion stayed within phase scope. The "Credentials & AWS scope" gray area was deliberately not discussed interactively (user judged it already settled by the existing BWS/docker-entrypoint infrastructure) — see Claude's Discretion above, not treated as out-of-scope or deferred to a future phase.
Phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud Context gathered: 2026-08-05