-- AWS Route 53 DNS sync — Phase 24 dedicated Postgres schema. -- -- Lays down the full Route 53 schema this phase's downstream plans (sync -- service, CRUD routes, health check, admin UI) will populate and consume. -- -- • Hosted zones (mirror of AWS Route 53 zones) -> route53_zones -- • Resource record sets (mirror of AWS records) -> route53_records -- • Append-only change ledger (drift + CRUD) -> route53_record_history -- • Append-only attempt audit log (D-03/D-07) -> route53_audit_log -- -- Retention is unbounded by design (D-08) — no purge job, no TTL, and no -- DELETE statement against route53_record_history or route53_audit_log -- anywhere in this phase, matching existing Pulse convention (itglue_writes, -- itglue_asset_audits, phishing audit_events are all forever-retained too). -- --------------------------------------------------------------------------- -- 1. route53_zones — mirror of AWS Route 53 hosted zones -- --------------------------------------------------------------------------- -- id is the AWS hosted zone id with the '/hostedzone/' prefix stripped — the -- natural stable identifier Route 53 already provides, no synthetic UUID -- needed. authoritative_name_servers backs the D-12 NS-delegation health -- check planned in 24-04. CREATE TABLE IF NOT EXISTS route53_zones ( id TEXT PRIMARY KEY, name TEXT NOT NULL, comment TEXT, private_zone BOOLEAN NOT NULL DEFAULT false, record_count INTEGER NOT NULL DEFAULT 0, authoritative_name_servers JSONB, raw_payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_route53_zones_is_deleted ON route53_zones(is_deleted); CREATE INDEX IF NOT EXISTS idx_route53_zones_name ON route53_zones(name); -- --------------------------------------------------------------------------- -- 2. route53_records — mirror of AWS Route 53 resource record sets -- --------------------------------------------------------------------------- -- record_key is the composite string `zoneId:name:type:setIdentifier` (empty -- string when there is no set identifier) — Route 53 recordsets are uniquely -- identified by zone+name+type+SetIdentifier; there is no AWS-side record id -- to use as a natural primary key. CREATE TABLE IF NOT EXISTS route53_records ( record_key TEXT PRIMARY KEY, zone_id TEXT NOT NULL REFERENCES route53_zones(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT NOT NULL, set_identifier TEXT, ttl INTEGER, resource_records JSONB, alias_target JSONB, raw_payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_route53_records_zone ON route53_records(zone_id); CREATE INDEX IF NOT EXISTS idx_route53_records_is_deleted ON route53_records(is_deleted); CREATE INDEX IF NOT EXISTS idx_route53_records_name_type ON route53_records(zone_id, name, type); -- --------------------------------------------------------------------------- -- 3. route53_record_history — append-only change ledger (D-06) -- --------------------------------------------------------------------------- -- Written by BOTH the sync service (source='sync_detected_drift', when a -- previously-synced record's live AWS value no longer matches Postgres) and -- the CRUD routes (source='pulse_crud', on every successful write). Distinct -- from route53_audit_log below: this table is a per-record timeline of -- resolved changes; the audit log below is a per-attempt ledger including -- failures. audit_log_id is a soft (non-FK) reference so a history row -- survives independent of the audit log's own lifecycle. CREATE TABLE IF NOT EXISTS route53_record_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), zone_id TEXT NOT NULL REFERENCES route53_zones(id) ON DELETE CASCADE, record_key TEXT NOT NULL, record_name TEXT NOT NULL, record_type TEXT NOT NULL, change_action TEXT NOT NULL CHECK (change_action IN ('create','update','delete')), before_value JSONB, after_value JSONB, source TEXT NOT NULL CHECK (source IN ('pulse_crud','sync_detected_drift')), changed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, changed_by_email TEXT, audit_log_id UUID, -- soft ref -> route53_audit_log(id), no hard FK changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_route53_record_history_record ON route53_record_history(record_key, changed_at DESC); CREATE INDEX IF NOT EXISTS idx_route53_record_history_source ON route53_record_history(source); CREATE INDEX IF NOT EXISTS idx_route53_record_history_zone ON route53_record_history(zone_id, changed_at DESC); -- --------------------------------------------------------------------------- -- 4. route53_audit_log — append-only attempt audit log (D-03/D-07) -- --------------------------------------------------------------------------- -- One row per attempted CRUD/sync operation, including failures. zone_id is -- deliberately NOT an FK here (unlike route53_record_history above) — a -- failed attempt against a zone that was never synced must still be -- recordable. D-03 accepted tradeoff: destructive record operations execute -- immediately with no staged approval gate; this table is the compensating -- post-hoc traceability control (actor, timestamp, before/after for every -- attempt), not a pre-write block. CREATE TABLE IF NOT EXISTS route53_audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), operation TEXT NOT NULL CHECK (operation IN ('create','update','delete','sync')), zone_id TEXT, record_key TEXT, record_name TEXT, record_type TEXT, before_value JSONB, after_value JSONB, performed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, performed_by_email TEXT, performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ, status TEXT NOT NULL CHECK (status IN ('pending','committed','failed')), aws_change_id TEXT, aws_change_status TEXT, aws_response JSONB, error_message TEXT ); CREATE INDEX IF NOT EXISTS ix_route53_audit_log_record ON route53_audit_log(zone_id, record_key, performed_at DESC); CREATE INDEX IF NOT EXISTS ix_route53_audit_log_status ON route53_audit_log(status); COMMENT ON TABLE route53_record_history IS 'Append-only per-record change timeline. source distinguishes CRUD writes (pulse_crud) from sync-detected drift (sync_detected_drift). Unbounded retention by design (D-08) — no purge job.'; COMMENT ON TABLE route53_audit_log IS 'Append-only per-attempt audit log including failures. D-03 accepted tradeoff: destructive record ops execute immediately with no pre-write approval gate; this table is the post-hoc compensating control. Unbounded retention by design (D-08) — no purge job.'; -- --------------------------------------------------------------------------- -- 5. integration_settings seed row (D-10) -- --------------------------------------------------------------------------- -- Display-only toggle on /admin/integrations — no sync/CRUD-blocking -- behavior anywhere in this phase (unlike PAX8's disable-gates-fullSync -- exception). Extends the existing seed list from migrations/081 rather -- than editing that committed file. INSERT INTO integration_settings (key, disabled) VALUES ('route53', false) ON CONFLICT (key) DO NOTHING;