# Phase 24: AWS Route 53 DNS Sync - Context

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

<domain>
## 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.

</domain>

<decisions>
## 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`).

</decisions>

<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.ts` — `sync_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_*.sql` — `integration_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>

<specifics>
## 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).

</specifics>

<deferred>
## 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.

</deferred>

---

*Phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud*
*Context gathered: 2026-08-05*
