Audit Log Schema

Append-only audit log JSONB on broadcasts and scheduled emails — every state change is recorded with a timestamp, action name, and per-action details.

Both broadcasts and scheduled_emails carry an audit_log JSONB column that records every state change. The log is append-only and best-effort — appends are async and never block the user-visible action, so a failure to write the audit entry doesn't roll back a successful cancel/send.

Schema

audit_log is an array of entries:

type AuditEntry = {
  at: string;        // ISO 8601 timestamp (UTC)
  action: string;    // see action types below
  details?: object;  // action-specific payload (optional)
};

Action types — Scheduled emails

  • created — initial creation. Details: { scheduled_for }.
  • rescheduledPATCH /scheduled/:id/reschedule succeeded. Details: { from, to, idempotency_token }.
  • cancelledDELETE /scheduled/:id succeeded. Details: { cancelled_by }.
  • send_started — BullMQ job activated and SES dispatch began.
  • sent — SES accepted the message. Details: { message_id }.
  • failed — SES rejected or processor error. Details: { error }.
  • send_nowPOST /scheduled/:id/send-now succeeded (rescheduled to "now + 1s").

Action types — Broadcasts

  • created — broadcast row created in draft state.
  • send_startedPOST /broadcasts/:id/send succeeded; status flipped draftsending. Details: { total_recipients }.
  • page_sent — one fan-out page completed. Details: { page, sent, failed, skipped_unsubscribed }.
  • cancel_requestedPOST /broadcasts/:id/cancel accepted. Details: { already_queued }.
  • cancelled — fan-out loop noticed status flipped to cancelled and stopped. Details: { pages_completed }.
  • completed — all recipients fanned out, status flipped to sent. Details: { total_recipients, total_failed }.
  • failed — fan-out aborted before completion (e.g. domain unverified mid-send). Details: { error }.

Reading the audit log

The audit_log array is included in GET /scheduled/:id and broadcast detail responses. The dashboard surfaces it as a timeline in the broadcast/scheduled details modal, so you can see every state change at a glance.

For programmatic use:

{
  "id": "uuid",
  "status": "cancelled",
  "audit_log": [
    { "at": "2026-04-15T10:00:00Z", "action": "created" },
    { "at": "2026-04-15T10:05:00Z", "action": "cancelled", "details": { "cancelled_by": "user-uuid" } }
  ]
}