npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mongoose-auditor

v0.1.2

Published

A powerful, production-ready Mongoose plugin to automatically track document changes (creates, updates, deletes) with an advanced feature set including field obfuscation, automatic history reversion, TTL logs, and powerful query helpers.

Readme

Mongoose Auditor

A powerful, production-ready Mongoose plugin to automatically track document changes (creates, updates, deletes) with an advanced feature set including field obfuscation, automatic history reversion, TTL logs, and powerful query helpers.

✨ Features

  • 🔬 Automatic Deep Diffing: Only logs the exact nested fields that changed (e.g. settings.notifications.sms: from false to true).
  • 🪝 Comprehensive Hook Support: Intercepts save, findOneAndUpdate, updateOne, updateMany, and findOneAndDelete.
  • 👤 Global Actor Context: Track who made the changes globally across the app.
  • 🛡️ Data Obfuscation: Mask sensitive data (like passwords) in the logs.
  • 🗑️ TTL Log Cleanup: Automatically delete old audit logs.
  • 🗄️ Custom Database Connection: Store logs in a completely separate database for compliance.
  • 📁 CSV/JSON Export: Built-in functions to export flattened CSV reports for SOC2/HIPAA compliance.
  • State Reversion: Easily rollback a document to its previous state with a single method call.
  • 📄 Pagination & Query Helpers: Fetch formatted history easily with built-in paginated statics.
  • Zero-Latency Writes: Asynchronous non-blocking architecture ensures main app speed isn't impacted.
  • 🎛️ Opt-in & Opt-out Tracking: Granular control via ignore and include arrays.
  • 🏷️ Custom Metadata: Inject IP Addresses, User Agents, or reasons directly into logs.
  • 📡 Event Emitters: Globally listen for auditLogCreated to fire off webhooks or analytics.

📦 Installation

Install the package via npm or yarn:

$ npm install mongoose-auditor
$ yarn add mongoose-auditor

🚀 Quick Setup

import mongoose from "mongoose";
import { auditTrail, AuditLog } from "mongoose-auditor";

const UserSchema = new mongoose.Schema({
  name: String,
  email: String,
  password: { type: String, select: false },
});

// Plug it in!
UserSchema.plugin(auditTrail, {
  obfuscate: ["password"], // Masks passwords in audit logs with "***"
  ignore: ["updatedAt"], // Never log this field
  retainDays: 90, // Auto-delete logs after 90 days
  getActor: () => globalCurrentUser?._id, // Globally resolve the actor making changes
});

const UserModel = mongoose.model("User", UserSchema);

🛠 Plugin Options

When initializing the plugin, you can pass an AuditTrailOptions object:

| Option | Type | Description | | :------------ | :-------------------- | :------------------------------------------------------------------------------------- | | ignore | string[] | Fields to completely ignore during diffing (e.g. ['updatedAt']). | | include | string[] | Opt-in mode. Only fields starting with these paths will be audited. | | obfuscate | string[] | Fields to mask with "***" in the audit log (e.g. ['password']). | | retainDays | number | Sets up a MongoDB TTL index to auto-delete logs after X days. | | background | boolean | Default true. Saves audit logs asynchronously so they don't block main app requests. | | connection | mongoose.Connection | Compiles the AuditLog model on a custom database connection. | | getActor | () => ObjectId | A callback function to dynamically resolve the actor ID (e.g. via AsyncLocalStorage). | | getMetadata | () => Record | Dynamically inject metadata into every log (e.g., IP Address, User Agent). |


🎭 Global Actor Context (AsyncLocalStorage)

Passing the actor/user ID manually to every save() or findOneAndUpdate() is tedious. By using the getActor option alongside Node's built-in AsyncLocalStorage, your audit trails can automatically figure out who triggered a database change without any extra code!

1. Create a Context Store:

import { AsyncLocalStorage } from "async_hooks";
export const requestContext = new AsyncLocalStorage<{ userId: string }>();

2. Set Context via Express Middleware:

app.use((req, res, next) => {
  const userId = req.user?._id; // Assuming auth middleware ran first

  // Wrap the request in the context
  requestContext.run({ userId }, () => {
    next();
  });
});

3. Configure the Plugin:

UserSchema.plugin(auditTrail, {
  getActor: () => requestContext.getStore()?.userId,
});

Now, calling await user.save() anywhere in your app will magically log the correct userId in the audit log!

Triggering Middleware Manually

If you aren't using the getActor global context, you can optionally pass the actor ID directly through query options when using Mongoose operations:

await UserModel.findOneAndUpdate(
  { _id: req.params.id },
  { $set: req.body },
  { actor: currentUserId }, // Passed dynamically via query options
);

🔄 Reverting History

You can instantly rollback a document to an older state using the .revert() method on an AuditLog instance.

const log = await AuditLog.findById(req.params.logId);

// Reverts the specific changes logged in this entry.
// Optionally pass an array of fields to NOT revert.
const restoredDocument = await log.revert(["updatedAt", "lastLogin"]);

Note: Reverting a "delete" operation will completely recreate the original document using the data captured when it was initially created! Reverting history does not trigger the plugin to create redundant, recursive audit logs.


📊 Common Query Functions (Table View)

The AuditLog model provides three static helper functions heavily optimized with MongoDB indexes to fetch historical data efficiently.

| Function Name | Description | Arguments to Pass | Return Type | | :------------------ | :------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | | getByDocument | Fetch the entire lifecycle history for a single, specific document. | documentId: string \| ObjectIdoptions: { populateActor?: boolean; skip?: number; limit?: number } | Query<AuditLogDocument[], AuditLogDocument> | | getByActor | Fetch all actions made globally by a specific actor/user. | actorId: string \| ObjectIdoptions: { populateActor?: boolean; skip?: number; limit?: number } | Query<AuditLogDocument[], AuditLogDocument> | | getByModel | Fetch all changes that occurred to an entire Mongoose Model (e.g. "User"). | modelName: stringoptions: { populateActor?: boolean; skip?: number; limit?: number } | Query<AuditLogDocument[], AuditLogDocument> |

Query Example (with Pagination)

import { AuditLog } from "mongoose-auditor";

// Get Page 2 of user 123's actions
const logs = await AuditLog.getByActor("123", {
  populateActor: true,
  limit: 10,
  skip: 10,
});

📡 Event Emitters (Webhooks)

You can globally listen to audit events across your entire application. This is extremely useful for streaming logs to Datadog, Slack, or triggering internal webhooks.

import { auditEvents } from "mongoose-auditor";

auditEvents.on("auditLogCreated", (log) => {
  console.log(`[ALERT] Action performed by ${log.actor}`);
  console.log(`Metadata IP:`, log.metadata?.ipAddress);
  // Example: SlackWebhook.send(log)
});

⚡ High-Scale Performance Best Practices

To ensure mongoose-auditor does not bottleneck your enterprise application, keep these three things in mind:

1️⃣ Asynchronous Writes (Enabled by Default)

The plugin has the background: true option enabled by default. This fires off the AuditLog.create() database commands asynchronously without blocking the main event loop. Your primary await user.save() finishes instantly without waiting for the audit log to save.

(Note: Set background: false if you require strict ACID-like guarantees that an audit log absolutely saved before responding to the user).

UserSchema.plugin(auditTrail, {
  background: false, // Forces the app to wait for the audit log to save before continuing
});

2️⃣ Blazing Fast Reads with .lean()

When rendering history tables for users on the frontend, reading thousands of raw Mongoose documents wastes severe CPU and RAM. Always append .lean() to the built-in query helpers for maximum speed.

const logs = await AuditLog.getByDocument(documentId, { limit: 100 }).lean(); // Extremely fast!

3️⃣ Dangerous updateMany Operations

If you run UserModel.updateMany({}, { active: true }) on 1,000,000 users, the audit trail MUST pull all 1,000,000 users into RAM to calculate the exact diffs. This will crash your server. For massive bulk updates, always bypass the audit trail:

await UserModel.updateMany({}, { active: true }, { __skipAudit: true });

📁 Data Exporting & Compliance (CSV / JSON)

For enterprise applications that require regular SOC2 or HIPAA compliance audits, you can easily export historical data.

The exportToCSV utility automatically flattens your audit trail. This means if a single update operation modified 5 different fields, the CSV will output 5 distinct rows (one per field change) for incredibly easy filtering in Excel.

import { exportToCSV, exportToJSON } from "mongoose-auditor";

app.get("/users/:id/export-csv", async (req, res) => {
  // 1. Fetch lightweight objects using .lean()
  const logs = await AuditLog.getByDocument(req.params.id).lean();

  // 2. Generate a flat CSV string (cast to 'any' when using lean)
  const csvString = exportToCSV(logs as any);

  // 3. Trigger download in browser
  res.header("Content-Type", "text/csv");
  res.attachment(`audit-logs.csv`);
  res.send(csvString);
});

🗑️ Reverting "Delete" Operations (Soft Deletes)

mongoose-auditor does not store full document backups. If you use findOneAndDelete(), the original document is destroyed and the "delete" audit log cannot be reversed.

To support reverting deletions, use the industry-standard Soft Delete pattern. Because soft-deleting is technically an update operation, the audit trail automatically tracks it!

1. Add deletedAt to your Schema (with an index)

const UserSchema = new mongoose.Schema({
  name: String,
  deletedAt: { type: Date, default: null, index: true }, // Index ensures fast queries!
});

2. Update instead of Delete

// Soft Delete the user
await UserModel.findOneAndUpdate(
  { _id: userId },
  { $set: { deletedAt: new Date() } },
);

3. Revert the Delete!

// Because it was just an update, you can revert it like normal!
const deleteLog = await AuditLog.findById("...");
await deleteLog.revert(); // deletedAt is restored to null!