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-timeline-audit

v2.0.0

Published

Mongoose plugin for comprehensive timeline and audit trail tracking with actor resolution and auto-trimming

Downloads

40

Readme

Mongoose Timeline Audit Plugin

A reusable Mongoose plugin that adds comprehensive timeline/audit trail tracking to any model.

Features

Track WHO, WHAT, WHEN - Complete audit trail with actor tracking ✅ Auto-trimming - Configurable event retention (keep latest N) ✅ Actor Resolution - Automatic detection of customer/admin/guest/system ✅ Flexible visibility - Timeline visible by default, optionally hide with hideByDefaultFramework-agnostic - Works with any Node.js/Mongoose project ✅ Stripe-style metadata - Clean, structured event metadata ✅ Zero dependencies - Only requires Mongoose ✅ Mongoose 8 & 9 Compatible - Fully tested with Mongoose 8+ and 9+ ✅ TypeScript First - Written in TypeScript with full type definitions included

Installation

npm install mongoose-timeline-audit
import timelineAuditPlugin from 'mongoose-timeline-audit';

Basic Usage

import mongoose from 'mongoose';
import timelineAuditPlugin from 'mongoose-timeline-audit';

const orderSchema = new mongoose.Schema({
  customerId: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer' },
  status: String,
  // ... other fields
});

// Apply plugin
orderSchema.plugin(timelineAuditPlugin, {
  ownerField: 'customerId',
  eventLimits: {
    'subscription.renewed': 10,  // Keep latest 10 renewals
    'payment.completed': 10,      // Keep latest 10 payments
  }
});

const Order = mongoose.model('Order', orderSchema);

TypeScript Support

The package is written in TypeScript and exports all types:

import timelineAuditPlugin, {
  type TimelinePluginOptions,
  type TimelineEvent,
  type ActorInfo,
  type ActorRole,
} from 'mongoose-timeline-audit';

// Full type safety for plugin options
const options: TimelinePluginOptions = {
  ownerField: 'customerId',
  eventLimits: {
    'order.cancelled': 5,
  },
};

orderSchema.plugin(timelineAuditPlugin, options);

Plugin Configuration

schema.plugin(timelineAuditPlugin, {
  // Field name that identifies the entity owner
  ownerField: 'customerId',  // default: 'customerId'

  // Timeline field name in schema
  fieldName: 'timeline',  // default: 'timeline'

  // Event retention limits (null = keep all)
  eventLimits: {
    'subscription.renewed': 10,
    'payment.completed': 10,
    'payment.failed': 5,
    // Events not listed keep all records (critical audit events)
  },

  // Enable/disable timeline
  enabled: true,  // default: true

  // Hide timeline field by default (requires .select('+timeline') to query)
  hideByDefault: false,  // default: false (visible by default)

  // Custom actor resolver (advanced)
  actorResolver: customResolverFunction,  // default: built-in resolver
});

Instance Methods

addTimelineEvent(event, description, request, metadata)

Add a timeline event to the document.

// Customer cancels their own order
order.addTimelineEvent(
  'order.cancelled',
  'Customer requested cancellation',
  request,  // Fastify/Express request object
  {
    immediate: true,
    refunded: true,
    refundAmount: 500,
  }
);

// Admin cancels order on behalf of customer
order.addTimelineEvent(
  'order.cancelled',
  'Admin cancelled due to policy violation',
  request,  // request.user.role = 'admin'
  { policyViolation: 'duplicate-order' }
);

// System automated action (cron job)
order.addTimelineEvent(
  'subscription.renewed',
  'Automatic renewal',
  null,  // null request = system action
  { renewalCount: 5 }
);

getTimelineEventsByActor(actorRole)

Get events filtered by actor role.

const adminActions = order.getTimelineEventsByActor('admin');
const customerActions = order.getTimelineEventsByActor('customer');
const systemActions = order.getTimelineEventsByActor('system');

getTimelineEventsByType(eventType)

Get events filtered by event type.

const cancellations = order.getTimelineEventsByType('order.cancelled');
const renewals = order.getTimelineEventsByType('subscription.renewed');

hasTimelineEvent(eventType)

Check if timeline has specific event.

if (order.hasTimelineEvent('order.cancelled')) {
  console.log('Order was cancelled at some point');
}

getLatestTimelineEvent()

Get most recent timeline event.

const latest = order.getLatestTimelineEvent();
console.log(`Last action: ${latest.event} by ${latest.metadata.actorRole}`);

Timeline Event Structure

{
  event: 'order.cancelled',
  description: 'Admin (on behalf of customer)',
  timestamp: Date('2025-01-16T10:30:00Z'),
  performedBy: ObjectId('507f1f77bcf86cd799439011'),  // User ID or null
  metadata: {
    actorRole: 'admin',
    onBehalfOf: '507f191e810c19729de860ea',
    organizationId: '507f1f77bcf86cd799439012',
    // Custom metadata
    immediate: true,
    refunded: true,
    refundAmount: 500,
  }
}

Actor Detection

The plugin automatically detects WHO performed an action:

| Scenario | actorRole | actorId | metadata | |----------|-----------|---------|----------| | Customer self-service | customer | userId | { selfService: true } | | Admin on behalf | admin | userId | { onBehalfOf: customerId, organizationId } | | Superadmin | superadmin | userId | { platformAdmin: true, onBehalfOf: customerId } | | Guest checkout | guest | null | { ipAddress, userAgent } | | System/Cron | system | null | { automated: true } |

Workflow Example

// workflows/cancel-order.workflow.js
export async function cancelOrderWorkflow(orderId, customerId, options = {}) {
  const { reason, immediate, request } = options;

  const order = await Order.findById(orderId);

  // ... business logic ...

  // Add timeline event with automatic actor tracking
  order.addTimelineEvent(
    'order.cancelled',
    immediate ? `Cancelled immediately: ${reason}` : `Cancellation scheduled: ${reason}`,
    request,  // Plugin auto-detects actor from request
    {
      immediate,
      reason,
    }
  );

  await order.save();

  return { order };
}

Querying Timeline Events

// Find orders cancelled by admin
const orders = await Order.find({
  'timeline.event': 'order.cancelled',
  'timeline.metadata.actorRole': 'admin',
});

// Find orders with system-automated renewals
const orders = await Order.find({
  'timeline.event': 'subscription.renewed',
  'timeline.metadata.automated': true,
});

// Timeline is included by default in queries
const order = await Order.findById(id);
console.log(order.timeline);

// If using hideByDefault: true, you need to explicitly select it
const orderWithTimeline = await Order.findById(id).select('+timeline');

Auto-Trimming Behavior

Events with configured limits automatically keep only the latest N events:

// Config: eventLimits: { 'subscription.renewed': 10 }

// After 15 renewals, timeline will only contain:
// - Latest 10 renewal events
// - All other event types (no limit)

// Critical audit events (cancellations, refunds) are ALWAYS kept

Visibility Control

By default, the timeline field is visible and included in all queries. You can change this behavior:

Hide Timeline by Default

If you want to exclude timeline from queries by default (for performance or security):

orderSchema.plugin(timelineAuditPlugin, {
  ownerField: 'customerId',
  hideByDefault: true,  // Exclude timeline from queries by default
  eventLimits: { /* ... */ }
});

// Now you need to explicitly select timeline
const order = await Order.findById(id);  // timeline NOT included
const orderWithTimeline = await Order.findById(id).select('+timeline');  // included

Keep Timeline Visible (Default)

orderSchema.plugin(timelineAuditPlugin, {
  ownerField: 'customerId',
  hideByDefault: false,  // or omit this option (default)
  eventLimits: { /* ... */ }
});

// Timeline is always included
const order = await Order.findById(id);  // timeline included
console.log(order.timeline);

When to use hideByDefault: true:

  • Large timeline arrays that impact query performance
  • Sensitive audit data that should only be accessed explicitly
  • API responses where timeline should be opt-in

When to use hideByDefault: false (default):

  • Timeline is part of normal data flow
  • You always need timeline data
  • Simplicity - no need to remember .select('+timeline')

Centralized Configuration

Use the provided configuration helpers to maintain DRY principles:

import timelineAuditPlugin, { getPluginConfig } from 'mongoose-timeline-audit';

// Use centralized config for standard models
orderSchema.plugin(timelineAuditPlugin, getPluginConfig('Order'));
enrollmentSchema.plugin(timelineAuditPlugin, getPluginConfig('Enrollment'));

// Override for specific models if needed
specialSchema.plugin(timelineAuditPlugin, getPluginConfig('Special', {
  eventLimits: {
    'special.event': 50,  // Custom limit
  }
}));

Custom Actor Resolver (Advanced)

function customActorResolver(request, ownerId) {
  // Custom logic to determine actor
  return {
    actorId: 'custom-id',
    actorRole: 'custom-role',
    metadata: { customField: 'value' },
  };
}

schema.plugin(timelineAuditPlugin, {
  actorResolver: customActorResolver,
});

License

MIT

Contributing

PRs welcome! Please ensure:

  • No breaking changes without major version bump
  • Tests for new features
  • Documentation updates