Append-only record of org-scoped events. Powers the audit UI and supports compliance review.
- Schema:
src/lib/db/schema/audit.ts — audit_logs table.
- Helper:
src/lib/api/audit.ts — logAudit(args) fire-and-forget insert.
- List endpoint:
GET /api/internal/audit?limit=50.
Defined in AuditAction (src/lib/api/audit.ts). Grouped by resource:
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
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,
}
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.
(org_id, created_at) — powers the dashboard timeline query.
(action) — filter by action type.
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.