wulf-pulse/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-CONTEXT.md

11 KiB

Phase 24: AWS Route 53 DNS Sync - Context

Gathered: 2026-08-05 Status: Ready for planning

## Phase Boundary

Sync 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.

## Implementation Decisions

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_events table. New migration introduces route53_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 source field: 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/route53 detail page for zones, records, and history — the existing per-integration pattern, not folded into an existing page.
  • D-10: /admin/integrations disable toggle for route53 is 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 dns module 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 to Dockerfile and docker-compose.yml that install the bws CLI and wrap the app's start command as bws run --project-id "$BWS_PROJECT_ID" -- node server.js when BWS_ACCESS_TOKEN is set, falling back to a plain node server.js otherwise. 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 normal process.env values by the time getRoute53Client()-style code runs. Researcher/planner should: (1) follow the exact existing lib/services/<name>-factory.ts + is<Name>Configured() pattern used by every other integration, reading credentials from process.env; (2) confirm the actual env var names with the user (e.g. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION, vs a ROUTE53_*-prefixed variant) before finalizing the factory — this wasn't locked in discussion; (3) add a ROUTE53_* (or AWS_*) 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 ChangeResourceRecordSets API 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 columns created_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 check
  • lib/services/datto-rmm-factory.ts, lib/services/datto-rmm-sync-service.ts — second reference implementation of the same pattern
  • lib/services/sync-scheduler.tssync_type union, ScheduleConfig, defaultSchedules array to extend for route53-incremental/route53-full
  • lib/services/integration-health.ts — health-check aggregator to extend
  • app/admin/sync/page.tsx — integration tile list (id/category/ product/description/href/logo/color) to extend with a route53 entry
  • migrations/081_*.sqlintegration_settings table backing the /admin/integrations disable 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/route53 page
  • components/admin/DetailModal.tsx — for record detail/history drill-down (formatted/raw tab pattern already established)
  • app/admin/sync/page.tsx tile array — extend with a route53 entry following the exact shape used for veeam/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 against lastTrackedModificationDateTime-style cursors, batched via postgresClient.bulkUpsert()
  • lib/services/sync-scheduler.ts — node-cron singleton; new sync types added to the sync_type string union and defaultSchedules seed array
  • lib/services/integration-health.ts — per-integration health check, cached ~5 minutes, read by both /admin/integrations and /admin/sync
  • migrations/081_*.sql integration_settings — disable-toggle table with disabled_by/disabled_at/disabled_reason audit 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 — add route53-incremental / route53-full to the sync_type union and defaultSchedules
  • lib/services/integration-health.ts — add a route53 health entry (auth check + last-sync age + the D-12 NS-delegation check)
  • app/admin/sync/page.tsx — add route53 tile; new app/admin/sync/route53/page.tsx detail 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 source tag (D-06).
## Deferred Ideas

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