Audit Log

Append-only record of org-scoped events. Powers the audit UI and supports compliance review.

Files

  • Schema: src/lib/db/schema/audit.tsaudit_logs table.
  • Helper: src/lib/api/audit.tslogAudit(args) fire-and-forget insert.
  • List endpoint: GET /api/internal/audit?limit=50.

Actions emitted

Defined in AuditAction (src/lib/api/audit.ts). Grouped by resource:

Users + sessions

Action Emitter
user.registered /api/auth/register
user.login (string constant defined; not yet emitted)
user.email_verified /api/auth/verify-email
user.password_reset /api/auth/reset-password (also bumps session_version)
user.sessions_revoked /api/internal/sessions DELETE
user.2fa_disabled /api/internal/two-factor/disable

API keys

Action Emitter
api_key.created /api/internal/api-keys POST
api_key.updated /api/internal/api-keys/[id] PATCH (rate limit, IP allow-list, etc.)
api_key.revoked /api/internal/api-keys DELETE

Domains

Action Emitter
domain.added /api/internal/domains POST
domain.removed /api/internal/domains/[id] DELETE
domain.verified /api/cron/domain-verify-poll (also v1 manual verify)
domain.unverified /api/cron/domain-verify-poll when DKIM revoked
domain.region_updated /api/internal/domains/[id]/region
domain.tenant_rebound /api/internal/domains/[id]/tenant
domain.warmup_started /api/internal/domains/[id]/warmup

Webhooks

Action Emitter
webhook.created /api/internal/webhooks POST
webhook.updated /api/internal/webhooks/[id] PATCH
webhook.removed /api/internal/webhooks/[id] DELETE
webhook.rotated /api/internal/webhooks/rotate
webhook.delivery_replayed /api/internal/webhook-deliveries/[id]/replay
webhook.test_fired /api/internal/webhooks/[id]/test

Templates + audiences + contacts

Action Emitter
template.created /api/internal/templates POST
template.updated /api/internal/templates/[id] PATCH
template.removed /api/internal/templates DELETE
audience.created /api/internal/audiences POST
audience.removed /api/internal/audiences DELETE
audience.csv_imported /api/internal/audiences/[id]/import-csv
audience.sent /api/internal/audiences/[id]/send
contact.created /api/internal/contacts POST
suppression.added /api/v1/suppressions POST + auto-suppression paths
suppression.removed /api/v1/suppressions DELETE

Tenants (platform mode)

Action Emitter
tenant.created /api/v1/tenants POST
tenant.suspended /api/v1/tenants/[id] PATCH (status: suspended)
tenant.unsuspended /api/v1/tenants/[id] PATCH (status: active)
tenant.quota_updated /api/v1/tenants/[id] PATCH (caps changed)

Messaging pools

Action Emitter
pool.created /api/internal/messaging-pools POST
pool.updated /api/internal/messaging-pools/[id] PATCH
pool.removed /api/internal/messaging-pools/[id] DELETE

SMS registration (10DLC + toll-free)

Action Emitter
brand.created /api/v1/brands POST
brand.updated /api/v1/brands/[id] PATCH
brand.removed /api/v1/brands/[id] DELETE
brand.verified /api/cron/sms-verify-poll
brand.unverified /api/cron/sms-verify-poll
campaign.created /api/v1/campaigns POST
campaign.updated /api/v1/campaigns/[id] PATCH
campaign.removed /api/v1/campaigns/[id] DELETE
campaign.verified /api/cron/sms-verify-poll
campaign.unverified /api/cron/sms-verify-poll
phone_number.provisioned /api/v1/phone-numbers POST
phone_number.released /api/v1/phone-numbers/[id] DELETE
phone_number.verified /api/cron/sms-verify-poll
phone_number.unverified /api/cron/sms-verify-poll

Team

Action Emitter
member.invited /api/internal/team/invite
member.joined /api/internal/team/accept
member.removed /api/internal/team/members DELETE
member.role_changed /api/internal/team/members PATCH

Billing / org

Action Emitter
plan.upgraded /api/webhooks/stripe on checkout.session.completed / customer.subscription.updated → active
plan.downgraded /api/webhooks/stripe on subscription deletion or non-active status
org.exported /api/internal/org/export/async (completion)
org.deleted /api/internal/org/delete

Shape

Each entry:

{
  id: "aud_...",
  orgId: "org_...",
  actorUserId: "usr_..." | null,
  action: "api_key.created",
  resourceType: "api_key" | null,
  resourceId: "key_..." | null,
  metadata: { ... } | null,
  ipAddress: "1.2.3.4" | null,       // from x-forwarded-for
  userAgent: "...",                  // request header
  createdAt: Date,
}

Usage

import { logAudit } from "@/lib/api/audit";

logAudit({
  orgId,
  actorUserId,
  action: "api_key.revoked",
  resourceType: "api_key",
  resourceId: keyId,
  req,
}).catch(() => {});

Always .catch(() => {}) — audit logging should never block the primary action.

Indexes

  • (org_id, created_at) — powers the dashboard timeline query.
  • (action) — filter by action type.

Retention

audit_logs rows older than 365 days are pruned nightly by /api/cron/retention. Tune AUDIT_LOG_RETENTION_DAYS (or edit the cron) if compliance requires longer retention. There's no archive-to-blob path yet — open follow-up for high-volume orgs.