Skip to Content
FeaturesAudit Logging

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

FieldTypeIndexed
actorIdStringyes
actorNameString
actorRoleString
tenantIdString | nullyes
tenantNameString | null
actionStringyes
resourceTypeStringyes
resourceIdStringyes
resourceNameString | null
metadataMixed
ipAddressString | null
createdAtDateyes (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

ActionTrigger
user.registeredSelf-signup completion
tenant.createdRegistration or admin create
user.loginSuccessful password login
user.profile_updatedPATCH /auth/v1/profile
user.password_changedAuthenticated password change
user.password_resetToken-based password reset

Admin — Users

ActionTrigger
user.createdAdmin creates a user
user.updatedAdmin edits a user
membership.addedAdmin adds user to tenant
membership.removedAdmin removes user from tenant

Admin — Tenants

ActionTrigger
tenant.createdAdmin creates tenant
tenant.updatedPATCH /admin/v1/tenants/:id
tenant.activatedPOST /admin/v1/tenants/:id/activate
tenant_strategy_config.updatedPATCH strategy config
tenant.setup_retriggeredPOST retrigger-setup
tenant.activity_planner_retriggeredPOST retrigger-activity-planner
tenant.deliverable_planner_retriggeredPOST retrigger-deliverable-planner
tenant.social_post_writer_forcedPOST force-social-post-writer
tenant.published_content_reingestedPOST reingest-published
tenant.crawl_settings_updatedPATCH crawl-settings
brand_assets.designer_config_updatedPATCH designer-config
tenant.deletion_triggeredPOST /admin/v1/tenants/:id/delete
tenant.deletion_cleanup_triggeredPOST /admin/v1/tenant-deletions/:id/cleanup

Admin — Agents & Skills

ActionTrigger
agent.updatedPATCH /admin/v1/agents/:id
skill.createdPOST /admin/v1/skills
skill.archivedDELETE /admin/v1/skills/:id
agent.skill_mappedPOST agent skill mapping
agent.skill_unmappedDELETE agent skill mapping

Admin — Billing

ActionTrigger
offering.createdPOST /admin/v1/offerings
offering.updatedPATCH /admin/v1/offerings/:id
offering.deletedDELETE /admin/v1/offerings/:id
plan.createdPOST /admin/v1/plans
plan.updatedPATCH /admin/v1/plans/:id
plan.deletedDELETE /admin/v1/plans/:id
region.createdPOST /admin/v1/regions
region.updatedPATCH /admin/v1/regions/:id
region.deletedDELETE /admin/v1/regions/:id
invoice.createdPOST create invoice
invoice.updatedPATCH invoice
subscription.createdPOST /admin/v1/subscriptions
subscription.updatedPATCH /admin/v1/subscriptions/:id
subscription.cancelledPOST cancel subscription
subscription.upsertedUpsert subscription (Razorpay webhook)

Admin — Platform Settings

ActionTrigger
deliverable_settings.updatedPATCH /admin/v1/deliverable-settings
design_defaults.updatedPATCH /admin/v1/design-defaults
template.createdPOST /admin/v1/templates
template.updatedPATCH /admin/v1/templates/:id
goal.updatedPATCH /admin/v1/goals/:id
notification.updatedPATCH /admin/v1/notifications/:id
push_notification.sentPOST /admin/v1/push (broadcast or tenant)
important_day.createdPOST /admin/v1/important-days
important_day.updatedPATCH /admin/v1/important-days/:id
important_day.deletedDELETE /admin/v1/important-days/:id
ActionTrigger
backlink_directory.createdPOST /admin/v1/backlink-directories
backlink_directory.updatedPATCH /admin/v1/backlink-directories/:id
backlink_directory.deletedDELETE /admin/v1/backlink-directories/:id
backlink_directory.test_submittedPOST submit test
backlink_directory_test_run.cancelledPOST cancel test run
backlink_directory.bulk_importedPOST bulk-import

Admin — Content

ActionTrigger
social_post.publish_cancelledPOST /admin/v1/content/social/:id/cancel

Tenant-Facing — Pipeline Approvals

ActionTrigger
context.approvedPOST /tenant/v1/context/:id/approve (also mobile)
strategy.approvedPOST /tenant/v1/strategy/:id/approve
deliverable_plan.approvedPOST /tenant/v1/plans/:id/approve (also mobile)

Tenant-Facing — Content

ActionTrigger
blog_post.updatedPATCH /tenant blog post
blog_post.approvedPOST approve blog post
blog_post.rejectedPOST reject blog post
blog_post.deletedDELETE blog post
blog_post.seo_optimize_triggeredPOST trigger SEO optimization
social_post.approvedPOST approve social post
social_post.rejectedPOST reject social post
social_post.publishedPOST /tenant/v1/social/:id/publish
social_post.scheduledPOST /tenant/v1/social/:id/schedule
social_post.schedule_cancelledPOST /tenant/v1/social/:id/cancel-schedule
social_post.deleted_from_platformDELETE from platform
social_post.deletedDELETE social post
landing_page.updatedPATCH landing page
landing_page.publishedPOST publish landing page
landing_page.deletedDELETE landing page
newsletter.sentPOST /tenant/v1/newsletters/:id/send

Tenant-Facing — Assets & Knowledge

ActionTrigger
brand_assets.updatedPATCH /tenant/v1/brand-assets
doc.uploadedPOST /tenant/v1/docs
doc.deletedDELETE /tenant/v1/docs/:id
media_asset.createdPOST /tenant/v1/media
media_asset.deletedDELETE /tenant/v1/media/:id
media.uploadedPOST /tenant/v1/media-library
media.deletedDELETE /tenant/v1/media-library/:id
media.updatedPATCH /tenant/v1/media-library/:id
knowledge_dataset.createdPOST /tenant/v1/knowledge
knowledge_dataset.updatedPATCH /tenant/v1/knowledge/:id
knowledge_dataset.deletedDELETE /tenant/v1/knowledge/:id
knowledge_file.uploadedPOST file upload to dataset
knowledge_file.deletedDELETE file from dataset
knowledge_crawl.startedPOST crawl a dataset

Tenant-Facing — Channels & Integrations

ActionTrigger
channel.createdPOST /tenant/v1/channels
channel.connectedOAuth callback in channel-connect.service.ts
channel.disconnectedPOST disconnect channel
channel.updatedPATCH channel settings
channel_health.explanation_requestedPOST request AI explanation
rag_config.updatedPATCH /admin/v1/rag-config (system only)

Tenant-Facing — CRM & Leads

ActionTrigger
competitor.createdPOST /tenant/v1/competitors
competitor.updatedPATCH /tenant/v1/competitors/:id
competitor.deletedDELETE /tenant/v1/competitors/:id
lead.createdPOST /tenant/v1/leads
lead.deletedDELETE /tenant/v1/leads/:id
insight.createdPOST /tenant/v1/insights
insight.deletedDELETE /tenant/v1/insights/:id

Tenant-Facing — Campaigns

ActionTrigger
campaign.createdPOST /tenant/v1/campaigns
campaign.updatedPATCH /tenant/v1/campaigns/:id
campaign.deletedDELETE /tenant/v1/campaigns/:id
campaign.status_changedPOST status change

Tenant-Facing — Billing & Credits

ActionTrigger
invoice.paidRazorpay invoice webhook
credits.topup_initiatedPOST /tenant/v1/credits/topup

Tenant-Facing — Preferences

ActionTrigger
notification_preferences.updatedPATCH /tenant/v1/notification-preferences
company_details.updatedPATCH /tenant/v1/company

Tenant-Facing — AI Chat

ActionTrigger
ai_chat_thread.archivedPATCH /aichat/v1/threads/:id/archive
ai_chat_thread.deletedDELETE /aichat/v1/threads/:id

Tenant-Facing — AI Visibility

ActionTrigger
ai_visibility.updatedPATCH AI visibility config
ai_visibility.deletedDELETE AI visibility item
ai_visibility.scan_triggeredPOST trigger scan
ActionTrigger
backlink.opportunity_status_updatedPATCH /tenant/v1/backlinks/:id/status
backlink.submission_retriedPOST /tenant/v1/backlinks/:id/retry-submission
backlink.opportunity_matcher_triggeredPOST /tenant/v1/backlinks/run-opportunity-matcher

DM Portal — Pipeline Reviews

ActionTrigger
context.rejectedPOST /dm/v1/context/:id/reject
strategy.rejectedPOST /dm/v1/strategy/:id/reject
deliverable_plan.dm_approvedPOST /dm/v1/calendar/plans/:id/approve
blog_post.dm_approvedPOST /dm/v1/blog/:id/approve
blog_post.dm_rejectedPOST /dm/v1/blog/:id/reject
blog_post.deletedDELETE /dm/v1/blog/:id
blog_post.status_changedPATCH /dm/v1/blog/:id/status
social_post.dm_approvedPOST /dm/v1/social/:id/approve
social_post.dm_rejectedPOST /dm/v1/social/:id/reject
social_post.status_changedPATCH /dm/v1/social/:id/status
social_post.deleted_from_platformDELETE from social platform
social_post.copy_editedPATCH /dm/v1/social/:id/copy
social_post.media_attachedPOST /dm/v1/social/:id/use-media
landing_page.approvedPOST /dm/v1/landing-pages/:id/approve
landing_page.rejectedPOST /dm/v1/landing-pages/:id/reject
landing_page.updatedPATCH /dm/v1/landing-pages/:id
landing_page.deletedDELETE /dm/v1/landing-pages/:id
newsletter.dm_approvedPOST /dm/v1/newsletters/:id/approve
newsletter.dm_rejectedPOST /dm/v1/newsletters/:id/reject
newsletter.status_changedPATCH /dm/v1/newsletters/:id/status

DM Portal — Plan Management

ActionTrigger
action_item.updatedPATCH /dm/v1/calendar/action-items/:id
deliverable_plan.goal_createdPOST /dm/v1/calendar/plans/:id/goals
deliverable_plan.goal_updatedPATCH /dm/v1/calendar/plans/:id/goals/:goalId
deliverable_plan.goal_deletedDELETE /dm/v1/calendar/plans/:id/goals/:goalId
deliverable_plan.template_updatedPATCH /dm/v1/calendar/plans/:id/template
deliverable_plan.template_toggledPOST /dm/v1/calendar/plans/:id/template/toggle

DM Portal — Activities & Campaigns

ActionTrigger
activity.createdPOST /dm/v1/activities
activity.status_changedPATCH /dm/v1/activities/:id/status
campaign.updatedPATCH /dm/v1/campaigns/:id
campaign.approvedPOST /dm/v1/campaigns/:id/approve
campaign.status_changedPATCH /dm/v1/campaigns/:id/status

DM Portal — Content Management

ActionTrigger
content_brief.createdPOST /dm/v1/content-briefs
content_brief.updatedPATCH /dm/v1/content-briefs/:id
content_brief.deletedDELETE /dm/v1/content-briefs/:id
content_brief.blog_generatedPOST /dm/v1/content-briefs/:id/generate-blog
keyword.createdPOST /dm/v1/keywords
keyword.updatedPATCH /dm/v1/keywords/:id
keyword.deletedDELETE /dm/v1/keywords/:id
keyword_group.createdPOST /dm/v1/keyword-groups
keyword_group.updatedPATCH /dm/v1/keyword-groups/:id
keyword_group.deletedDELETE /dm/v1/keyword-groups/:id
report.createdPOST /dm/v1/reports

DM Portal — Search Terms

ActionTrigger
search_term_classification.updatedPATCH /dm/v1/search-terms/classifications/:id
search_term_classifications.pushedPOST /dm/v1/search-terms/push

DM Portal — Contacts & Media

ActionTrigger
contact.createdPOST /dm/v1/contacts
contact.updatedPATCH /dm/v1/contacts/:id
contact.deletedDELETE /dm/v1/contacts/:id
media.uploadedPOST /dm/v1/media

DM Portal — AI Visibility

ActionTrigger
ai_visibility_prompt.createdPOST /dm/v1/ai-visibility/prompts
ai_visibility_prompt.updatedPATCH /dm/v1/ai-visibility/prompts/:id
ai_visibility_prompt.deletedDELETE /dm/v1/ai-visibility/prompts/:id
ai_visibility_platform.updatedPATCH /dm/v1/ai-visibility/platforms/:id

DM Portal — Misc

ActionTrigger
tenant.switchedPOST /dm/v1/tenant/switch

Mobile App

ActionTrigger
context.approvedMobile approve context
context.revision_requestedMobile request context revision
strategy.status_updatedMobile strategy status change
strategy.revision_requestedMobile request strategy revision
deliverable_plan.approvedMobile approve plan
notification.updatedMobile mark notification read
push_token.registeredMobile register FCM token
push_token.unregisteredMobile unregister FCM token

Intentionally Not Logged

The following events are deliberately excluded:

Excluded eventReason
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/webhookRazorpay server-to-server; no human actor
POST /dm/v1/search-terms/triggerBullMQ enqueue only; job itself is the record
Agent/worker completionsTracked in AgentRun Prisma table, not audit log

Coverage by Domain

DomainRouter filesCoverage
Auth & Identityauth.ts100%
Admin — Tenantsadmin/tenants.ts, admin/tenant-deletions.ts100%
Admin — Usersadmin/users.ts100%
Admin — Agents & Skillsadmin/agents.ts100%
Admin — Billingadmin/billing.ts100%
Admin — Platform Settingsadmin/deliverable-settings.ts, admin/design-defaults.ts, admin/templates.ts, admin/goals.ts, admin/notifications.ts, admin/push.ts, admin/important-days.ts100%
Admin — Backlinksadmin/backlink-directories.ts100%
Admin — Contentadmin/content.ts100%
Tenant — Pipelinetenant/main.ts100%
Tenant — Creditstenant/credits.ts100% (topup initiation; webhook excluded by design)
Tenant — Contentblog.ts, social.ts, landing-pages.ts, newsletters.ts100% (publish/schedule/cancel-schedule added Apr 2026)
Tenant — Assetsbrand-assets.ts, docs.ts, media.ts, media-library.ts100%
Tenant — Knowledgeknowledge.ts100%
Tenant — Channelschannels.ts, channel-connect.service.ts100%
Tenant — CRMcompetitors.ts, leads.ts, insights.ts100%
Tenant — Campaignscampaigns.ts100%
Tenant — Backlinkstenant-backlinks.ts100% (submission_retried + opportunity_matcher_triggered added Apr 2026)
Tenant — AI featuresai-chat.ts, ai-visibility.ts100% (creation excluded by design)
DM — Reviewsdm/context.ts, dm/strategy.ts, dm/blog.ts, dm/social.ts, dm/landing-pages.ts, dm/newsletters.ts100% (media_attached added Apr 2026)
DM — Plan Mgmtdm/calendar.ts100%
DM — Activitiesdm/activities.ts100%
DM — Contentdm/content-briefs.ts, dm/keywords.ts, dm/campaigns.ts100% (blog_generated added Apr 2026)
DM — Search Termsdm/search-terms.ts100% (trigger excluded by design)
DM — Contacts/Mediadm/contacts.ts, dm/media.ts100%
DM — AI Visibilitydm/ai-visibility.ts100%
Mobilemobile.ts100%

Gotcha: Two requireSuperAdmin Implementations

There are two different implementations of requireSuperAdmin in the admin routers:

Import sourceReturn 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).

© 2026 Leadmetrics — Internal use only