Audit Logging
Overview
Every meaningful user and system action across all Leadmetrics portals and API routers is durably recorded in a MongoDB audit_logs collection. The log is permanent — never deleted or truncated. Entries are also published to a Redis pub/sub channel so super-admin UIs can stream them in real time.
Related: observability.md (tracing, Loki, Grafana) | database-mongo.md
Implementation
Package location
packages/nosqldb/src/audit.ts — exported from @leadmetrics/nosqldb
Function signature
import { writeAuditLog } from "@leadmetrics/nosqldb";
writeAuditLog({
actorId: string; // user ID or system identifier
actorName: string; // display name
actorRole: string; // "super_admin" | "user" | "dm_user" | etc.
tenantId?: string | null; // omit for platform-level events
tenantName?: string | null;
action: string; // dot-notation: "resource.verb"
resourceType: string; // "tenant" | "blog_post" | "social_post" | etc.
resourceId: string; // primary key of the affected resource
resourceName?: string | null; // human-readable label
metadata?: Record<string, unknown>; // event-specific fields
ipAddress?: string | null;
});writeAuditLog is fire-and-forget — it never throws or blocks the calling route handler. Audit failures are silently swallowed so they never affect request outcomes.
MongoDB schema
Collection: audit_logs
| Field | Type | Indexed |
|---|---|---|
actorId | String | yes |
actorName | String | |
actorRole | String | |
tenantId | String | null | yes |
tenantName | String | null | |
action | String | yes |
resourceType | String | yes |
resourceId | String | yes |
resourceName | String | null | |
metadata | Mixed | |
ipAddress | String | null | |
createdAt | Date | yes (desc) |
Compound indexes: (actorId, createdAt), (tenantId, createdAt), (action, createdAt), (resourceType, resourceId, createdAt).
Real-time streaming
After writing to MongoDB, the document is published to the Redis channel audit:events. The /admin/v1/audit-logs/stream SSE endpoint subscribes to this channel, allowing the manage portal to show a live feed of platform events.
Writing Audit Logs — Patterns
Admin routes (import from ../../lib/auth)
requireSuperAdmin from lib/auth returns { sub: string; role: string } | null.
const actorId = await requireSuperAdmin(req, reply);
if (!actorId) return;
const actor = await db.user.findUnique({ where: { id: actorId.sub }, select: { name: true, role: true } });
writeAuditLog({
actorId: actorId.sub,
actorName: actor?.name ?? "Super Admin",
actorRole: actor?.role ?? "super_admin",
action: "tenant.updated",
resourceType: "tenant",
resourceId: tenantId,
ipAddress: req.ip,
});Admin routes (import from ./_shared)
requireSuperAdmin from ./_shared returns userId: string | null (plain string). Use userId directly (not .sub).
DM routes
const actor = await requireDMAccess(req, reply);
if (!actor) return;
writeAuditLog({
actorId: actor.sub,
actorName: "User",
actorRole: actor.role ?? "user",
tenantId: tenantId,
action: "blog_post.dm_approved",
resourceType: "blog_post",
resourceId: postId,
});Tenant-facing routes
const actor = await requireTenantUser(req, reply);
if (!actor) return;
writeAuditLog({
actorId: actor.sub,
actorName: "User",
actorRole: actor.role ?? "user",
tenantId: actor.tenantId,
action: "context.approved",
resourceType: "tenant_context",
resourceId: contextId,
});Action Inventory
All actions follow resource_type.verb naming. 141 unique events across 40+ router files.
Auth & Identity
| Action | Trigger |
|---|---|
user.registered | Self-signup completion |
tenant.created | Registration or admin create |
user.login | Successful password login |
user.profile_updated | PATCH /auth/v1/profile |
user.password_changed | Authenticated password change |
user.password_reset | Token-based password reset |
Admin — Users
| Action | Trigger |
|---|---|
user.created | Admin creates a user |
user.updated | Admin edits a user |
membership.added | Admin adds user to tenant |
membership.removed | Admin removes user from tenant |
Admin — Tenants
| Action | Trigger |
|---|---|
tenant.created | Admin creates tenant |
tenant.updated | PATCH /admin/v1/tenants/:id |
tenant.activated | POST /admin/v1/tenants/:id/activate |
tenant_strategy_config.updated | PATCH strategy config |
tenant.setup_retriggered | POST retrigger-setup |
tenant.activity_planner_retriggered | POST retrigger-activity-planner |
tenant.deliverable_planner_retriggered | POST retrigger-deliverable-planner |
tenant.social_post_writer_forced | POST force-social-post-writer |
tenant.published_content_reingested | POST reingest-published |
tenant.crawl_settings_updated | PATCH crawl-settings |
brand_assets.designer_config_updated | PATCH designer-config |
tenant.deletion_triggered | POST /admin/v1/tenants/:id/delete |
tenant.deletion_cleanup_triggered | POST /admin/v1/tenant-deletions/:id/cleanup |
Admin — Agents & Skills
| Action | Trigger |
|---|---|
agent.updated | PATCH /admin/v1/agents/:id |
skill.created | POST /admin/v1/skills |
skill.archived | DELETE /admin/v1/skills/:id |
agent.skill_mapped | POST agent skill mapping |
agent.skill_unmapped | DELETE agent skill mapping |
Admin — Billing
| Action | Trigger |
|---|---|
offering.created | POST /admin/v1/offerings |
offering.updated | PATCH /admin/v1/offerings/:id |
offering.deleted | DELETE /admin/v1/offerings/:id |
plan.created | POST /admin/v1/plans |
plan.updated | PATCH /admin/v1/plans/:id |
plan.deleted | DELETE /admin/v1/plans/:id |
region.created | POST /admin/v1/regions |
region.updated | PATCH /admin/v1/regions/:id |
region.deleted | DELETE /admin/v1/regions/:id |
invoice.created | POST create invoice |
invoice.updated | PATCH invoice |
subscription.created | POST /admin/v1/subscriptions |
subscription.updated | PATCH /admin/v1/subscriptions/:id |
subscription.cancelled | POST cancel subscription |
subscription.upserted | Upsert subscription (Razorpay webhook) |
Admin — Platform Settings
| Action | Trigger |
|---|---|
deliverable_settings.updated | PATCH /admin/v1/deliverable-settings |
design_defaults.updated | PATCH /admin/v1/design-defaults |
template.created | POST /admin/v1/templates |
template.updated | PATCH /admin/v1/templates/:id |
goal.updated | PATCH /admin/v1/goals/:id |
notification.updated | PATCH /admin/v1/notifications/:id |
push_notification.sent | POST /admin/v1/push (broadcast or tenant) |
important_day.created | POST /admin/v1/important-days |
important_day.updated | PATCH /admin/v1/important-days/:id |
important_day.deleted | DELETE /admin/v1/important-days/:id |
Admin — Backlink Directories
| Action | Trigger |
|---|---|
backlink_directory.created | POST /admin/v1/backlink-directories |
backlink_directory.updated | PATCH /admin/v1/backlink-directories/:id |
backlink_directory.deleted | DELETE /admin/v1/backlink-directories/:id |
backlink_directory.test_submitted | POST submit test |
backlink_directory_test_run.cancelled | POST cancel test run |
backlink_directory.bulk_imported | POST bulk-import |
Admin — Content
| Action | Trigger |
|---|---|
social_post.publish_cancelled | POST /admin/v1/content/social/:id/cancel |
Tenant-Facing — Pipeline Approvals
| Action | Trigger |
|---|---|
context.approved | POST /tenant/v1/context/:id/approve (also mobile) |
strategy.approved | POST /tenant/v1/strategy/:id/approve |
deliverable_plan.approved | POST /tenant/v1/plans/:id/approve (also mobile) |
Tenant-Facing — Content
| Action | Trigger |
|---|---|
blog_post.updated | PATCH /tenant blog post |
blog_post.approved | POST approve blog post |
blog_post.rejected | POST reject blog post |
blog_post.deleted | DELETE blog post |
blog_post.seo_optimize_triggered | POST trigger SEO optimization |
social_post.approved | POST approve social post |
social_post.rejected | POST reject social post |
social_post.published | POST /tenant/v1/social/:id/publish |
social_post.scheduled | POST /tenant/v1/social/:id/schedule |
social_post.schedule_cancelled | POST /tenant/v1/social/:id/cancel-schedule |
social_post.deleted_from_platform | DELETE from platform |
social_post.deleted | DELETE social post |
landing_page.updated | PATCH landing page |
landing_page.published | POST publish landing page |
landing_page.deleted | DELETE landing page |
newsletter.sent | POST /tenant/v1/newsletters/:id/send |
Tenant-Facing — Assets & Knowledge
| Action | Trigger |
|---|---|
brand_assets.updated | PATCH /tenant/v1/brand-assets |
doc.uploaded | POST /tenant/v1/docs |
doc.deleted | DELETE /tenant/v1/docs/:id |
media_asset.created | POST /tenant/v1/media |
media_asset.deleted | DELETE /tenant/v1/media/:id |
media.uploaded | POST /tenant/v1/media-library |
media.deleted | DELETE /tenant/v1/media-library/:id |
media.updated | PATCH /tenant/v1/media-library/:id |
knowledge_dataset.created | POST /tenant/v1/knowledge |
knowledge_dataset.updated | PATCH /tenant/v1/knowledge/:id |
knowledge_dataset.deleted | DELETE /tenant/v1/knowledge/:id |
knowledge_file.uploaded | POST file upload to dataset |
knowledge_file.deleted | DELETE file from dataset |
knowledge_crawl.started | POST crawl a dataset |
Tenant-Facing — Channels & Integrations
| Action | Trigger |
|---|---|
channel.created | POST /tenant/v1/channels |
channel.connected | OAuth callback in channel-connect.service.ts |
channel.disconnected | POST disconnect channel |
channel.updated | PATCH channel settings |
channel_health.explanation_requested | POST request AI explanation |
rag_config.updated | PATCH /admin/v1/rag-config (system only) |
Tenant-Facing — CRM & Leads
| Action | Trigger |
|---|---|
competitor.created | POST /tenant/v1/competitors |
competitor.updated | PATCH /tenant/v1/competitors/:id |
competitor.deleted | DELETE /tenant/v1/competitors/:id |
lead.created | POST /tenant/v1/leads |
lead.deleted | DELETE /tenant/v1/leads/:id |
insight.created | POST /tenant/v1/insights |
insight.deleted | DELETE /tenant/v1/insights/:id |
Tenant-Facing — Campaigns
| Action | Trigger |
|---|---|
campaign.created | POST /tenant/v1/campaigns |
campaign.updated | PATCH /tenant/v1/campaigns/:id |
campaign.deleted | DELETE /tenant/v1/campaigns/:id |
campaign.status_changed | POST status change |
Tenant-Facing — Billing & Credits
| Action | Trigger |
|---|---|
invoice.paid | Razorpay invoice webhook |
credits.topup_initiated | POST /tenant/v1/credits/topup |
Tenant-Facing — Preferences
| Action | Trigger |
|---|---|
notification_preferences.updated | PATCH /tenant/v1/notification-preferences |
company_details.updated | PATCH /tenant/v1/company |
Tenant-Facing — AI Chat
| Action | Trigger |
|---|---|
ai_chat_thread.archived | PATCH /aichat/v1/threads/:id/archive |
ai_chat_thread.deleted | DELETE /aichat/v1/threads/:id |
Tenant-Facing — AI Visibility
| Action | Trigger |
|---|---|
ai_visibility.updated | PATCH AI visibility config |
ai_visibility.deleted | DELETE AI visibility item |
ai_visibility.scan_triggered | POST trigger scan |
Tenant-Facing — Backlinks
| Action | Trigger |
|---|---|
backlink.opportunity_status_updated | PATCH /tenant/v1/backlinks/:id/status |
backlink.submission_retried | POST /tenant/v1/backlinks/:id/retry-submission |
backlink.opportunity_matcher_triggered | POST /tenant/v1/backlinks/run-opportunity-matcher |
DM Portal — Pipeline Reviews
| Action | Trigger |
|---|---|
context.rejected | POST /dm/v1/context/:id/reject |
strategy.rejected | POST /dm/v1/strategy/:id/reject |
deliverable_plan.dm_approved | POST /dm/v1/calendar/plans/:id/approve |
blog_post.dm_approved | POST /dm/v1/blog/:id/approve |
blog_post.dm_rejected | POST /dm/v1/blog/:id/reject |
blog_post.deleted | DELETE /dm/v1/blog/:id |
blog_post.status_changed | PATCH /dm/v1/blog/:id/status |
social_post.dm_approved | POST /dm/v1/social/:id/approve |
social_post.dm_rejected | POST /dm/v1/social/:id/reject |
social_post.status_changed | PATCH /dm/v1/social/:id/status |
social_post.deleted_from_platform | DELETE from social platform |
social_post.copy_edited | PATCH /dm/v1/social/:id/copy |
social_post.media_attached | POST /dm/v1/social/:id/use-media |
landing_page.approved | POST /dm/v1/landing-pages/:id/approve |
landing_page.rejected | POST /dm/v1/landing-pages/:id/reject |
landing_page.updated | PATCH /dm/v1/landing-pages/:id |
landing_page.deleted | DELETE /dm/v1/landing-pages/:id |
newsletter.dm_approved | POST /dm/v1/newsletters/:id/approve |
newsletter.dm_rejected | POST /dm/v1/newsletters/:id/reject |
newsletter.status_changed | PATCH /dm/v1/newsletters/:id/status |
DM Portal — Plan Management
| Action | Trigger |
|---|---|
action_item.updated | PATCH /dm/v1/calendar/action-items/:id |
deliverable_plan.goal_created | POST /dm/v1/calendar/plans/:id/goals |
deliverable_plan.goal_updated | PATCH /dm/v1/calendar/plans/:id/goals/:goalId |
deliverable_plan.goal_deleted | DELETE /dm/v1/calendar/plans/:id/goals/:goalId |
deliverable_plan.template_updated | PATCH /dm/v1/calendar/plans/:id/template |
deliverable_plan.template_toggled | POST /dm/v1/calendar/plans/:id/template/toggle |
DM Portal — Activities & Campaigns
| Action | Trigger |
|---|---|
activity.created | POST /dm/v1/activities |
activity.status_changed | PATCH /dm/v1/activities/:id/status |
campaign.updated | PATCH /dm/v1/campaigns/:id |
campaign.approved | POST /dm/v1/campaigns/:id/approve |
campaign.status_changed | PATCH /dm/v1/campaigns/:id/status |
DM Portal — Content Management
| Action | Trigger |
|---|---|
content_brief.created | POST /dm/v1/content-briefs |
content_brief.updated | PATCH /dm/v1/content-briefs/:id |
content_brief.deleted | DELETE /dm/v1/content-briefs/:id |
content_brief.blog_generated | POST /dm/v1/content-briefs/:id/generate-blog |
keyword.created | POST /dm/v1/keywords |
keyword.updated | PATCH /dm/v1/keywords/:id |
keyword.deleted | DELETE /dm/v1/keywords/:id |
keyword_group.created | POST /dm/v1/keyword-groups |
keyword_group.updated | PATCH /dm/v1/keyword-groups/:id |
keyword_group.deleted | DELETE /dm/v1/keyword-groups/:id |
report.created | POST /dm/v1/reports |
DM Portal — Search Terms
| Action | Trigger |
|---|---|
search_term_classification.updated | PATCH /dm/v1/search-terms/classifications/:id |
search_term_classifications.pushed | POST /dm/v1/search-terms/push |
DM Portal — Contacts & Media
| Action | Trigger |
|---|---|
contact.created | POST /dm/v1/contacts |
contact.updated | PATCH /dm/v1/contacts/:id |
contact.deleted | DELETE /dm/v1/contacts/:id |
media.uploaded | POST /dm/v1/media |
DM Portal — AI Visibility
| Action | Trigger |
|---|---|
ai_visibility_prompt.created | POST /dm/v1/ai-visibility/prompts |
ai_visibility_prompt.updated | PATCH /dm/v1/ai-visibility/prompts/:id |
ai_visibility_prompt.deleted | DELETE /dm/v1/ai-visibility/prompts/:id |
ai_visibility_platform.updated | PATCH /dm/v1/ai-visibility/platforms/:id |
DM Portal — Misc
| Action | Trigger |
|---|---|
tenant.switched | POST /dm/v1/tenant/switch |
Mobile App
| Action | Trigger |
|---|---|
context.approved | Mobile approve context |
context.revision_requested | Mobile request context revision |
strategy.status_updated | Mobile strategy status change |
strategy.revision_requested | Mobile request strategy revision |
deliverable_plan.approved | Mobile approve plan |
notification.updated | Mobile mark notification read |
push_token.registered | Mobile register FCM token |
push_token.unregistered | Mobile unregister FCM token |
Intentionally Not Logged
The following events are deliberately excluded:
| Excluded event | Reason |
|---|---|
POST /aichat/v1/threads (create) | High-volume; every user message would flood the log |
POST /aichat/v1/threads/:id (continue) | Same — message-level volume |
POST /tenant/v1/credits/topup/webhook | Razorpay server-to-server; no human actor |
POST /dm/v1/search-terms/trigger | BullMQ enqueue only; job itself is the record |
| Agent/worker completions | Tracked in AgentRun Prisma table, not audit log |
Coverage by Domain
| Domain | Router files | Coverage |
|---|---|---|
| Auth & Identity | auth.ts | 100% |
| Admin — Tenants | admin/tenants.ts, admin/tenant-deletions.ts | 100% |
| Admin — Users | admin/users.ts | 100% |
| Admin — Agents & Skills | admin/agents.ts | 100% |
| Admin — Billing | admin/billing.ts | 100% |
| Admin — Platform Settings | admin/deliverable-settings.ts, admin/design-defaults.ts, admin/templates.ts, admin/goals.ts, admin/notifications.ts, admin/push.ts, admin/important-days.ts | 100% |
| Admin — Backlinks | admin/backlink-directories.ts | 100% |
| Admin — Content | admin/content.ts | 100% |
| Tenant — Pipeline | tenant/main.ts | 100% |
| Tenant — Credits | tenant/credits.ts | 100% (topup initiation; webhook excluded by design) |
| Tenant — Content | blog.ts, social.ts, landing-pages.ts, newsletters.ts | 100% (publish/schedule/cancel-schedule added Apr 2026) |
| Tenant — Assets | brand-assets.ts, docs.ts, media.ts, media-library.ts | 100% |
| Tenant — Knowledge | knowledge.ts | 100% |
| Tenant — Channels | channels.ts, channel-connect.service.ts | 100% |
| Tenant — CRM | competitors.ts, leads.ts, insights.ts | 100% |
| Tenant — Campaigns | campaigns.ts | 100% |
| Tenant — Backlinks | tenant-backlinks.ts | 100% (submission_retried + opportunity_matcher_triggered added Apr 2026) |
| Tenant — AI features | ai-chat.ts, ai-visibility.ts | 100% (creation excluded by design) |
| DM — Reviews | dm/context.ts, dm/strategy.ts, dm/blog.ts, dm/social.ts, dm/landing-pages.ts, dm/newsletters.ts | 100% (media_attached added Apr 2026) |
| DM — Plan Mgmt | dm/calendar.ts | 100% |
| DM — Activities | dm/activities.ts | 100% |
| DM — Content | dm/content-briefs.ts, dm/keywords.ts, dm/campaigns.ts | 100% (blog_generated added Apr 2026) |
| DM — Search Terms | dm/search-terms.ts | 100% (trigger excluded by design) |
| DM — Contacts/Media | dm/contacts.ts, dm/media.ts | 100% |
| DM — AI Visibility | dm/ai-visibility.ts | 100% |
| Mobile | mobile.ts | 100% |
Gotcha: Two requireSuperAdmin Implementations
There are two different implementations of requireSuperAdmin in the admin routers:
| Import source | Return type |
|---|---|
import { requireSuperAdmin } from "./_shared" | userId: string | null |
import { requireSuperAdmin } from "../../lib/auth" | { sub: string; role: string } | null |
Files that import from lib/auth: admin/deliverable-settings.ts, admin/design-defaults.ts, admin/important-days.ts.
All other admin routers import from ./_shared and receive a plain string.
When writing audit logs in a lib/auth file, use actorId.sub (not actorId).