← back to Trademarks Copyright

db/comms_compliance.sql

45 lines

-- comms_compliance.sql — applied 2026-05-03 by comms-compliance officer.
--
-- Two append-only tables that gate every outbound message:
--   comms_suppression — anyone who unsubscribed, hard-bounced, complained,
--                       or was added to a regulator DNC. Pre-flight check.
--   comms_send_audit  — one row per send decision (sent OR blocked), with
--                       SHA-256 of the recipient identifier (no PII). Forensic
--                       proof of compliance per FTC consent-decree guidance.
--
-- Wire-up note: the drops mailer (src/lib/email.ts + src/app/api/drops/send)
-- does NOT yet call into these tables. Roadmap item — when we wire the drops
-- send path through isSendAllowed() / logDecision(), this is where it logs.
-- Schema definitions match the canonical layout in
-- ~/.claude/agents/comms-compliance.md so future projects sharing PG can join.

CREATE TABLE IF NOT EXISTS comms_suppression (
  id              BIGSERIAL PRIMARY KEY,
  channel         TEXT NOT NULL CHECK (channel IN ('email','sms','voice')),
  identifier      TEXT NOT NULL,                  -- email or E.164 phone (raw, scrubbed if needed)
  identifier_hash TEXT NOT NULL,                  -- SHA-256(identifier) for joins without exposing PII
  reason          TEXT NOT NULL CHECK (reason IN ('unsubscribe','dnc','hard_bounce','complaint','manual','tcpa_revoke','ccpa_delete')),
  source          TEXT,                           -- federal_dnc / utah_cpr / fbl_gmail / one_click / etc.
  added_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  campaign_id     TEXT,                           -- if added during a specific campaign
  notes           TEXT,
  UNIQUE (channel, identifier)
);
CREATE INDEX IF NOT EXISTS idx_comms_suppression_hash ON comms_suppression(identifier_hash);

CREATE TABLE IF NOT EXISTS comms_send_audit (
  id                  BIGSERIAL PRIMARY KEY,
  channel             TEXT NOT NULL CHECK (channel IN ('email','sms','voice')),
  campaign_id         TEXT NOT NULL,
  identifier_hash     TEXT NOT NULL,              -- SHA-256, never raw recipient
  decision            TEXT NOT NULL CHECK (decision IN ('sent','blocked_dnc','blocked_suppression','blocked_no_consent','blocked_quiet_hours','blocked_other','failed_send')),
  block_source        TEXT,                       -- which list flagged it
  scrubbed_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  scrub_version       TEXT NOT NULL,              -- date-stamp of the DNC snapshot used
  consent_log_id      BIGINT,
  message_template_id TEXT,
  notes               TEXT
);
CREATE INDEX IF NOT EXISTS idx_comms_send_audit_campaign ON comms_send_audit(campaign_id);
CREATE INDEX IF NOT EXISTS idx_comms_send_audit_decision ON comms_send_audit(decision, scrubbed_at);