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

@whatworks/payload-activity-log

v0.2.0

Published

Payload plugin that records a chronological activity feed — document creates, updates, deletes, and user logins/logouts — with links to the affected document and version instead of full data snapshots.

Readme

@whatworks/payload-activity-log

 

Payload plugin that records a chronological activity feed for your whole CMS — who did what, when, across every collection and global. Think WordPress's Simple History, for Payload.

  • Logs document creates, updates, trashes, restores, and deletes, plus user logins and logouts (opt-out, not opt-in).
  • Shows document titles and user emails, not IDs — both captured at event time, so they survive deletion.
  • No full-document snapshots by default. Each entry links to the version the change produced (when versions are enabled) instead of duplicating document data. Only permanent deletes store a snapshot — the one moment the data would otherwise be lost, since Payload deletes a document's versions with it.
  • Records the changed field names on updates ("title, content") without storing values.
  • Supports multiple auth collections — actors are stored as a polymorphic reference plus a label.
  • Composes with @whatworks/payload-audit-fields: reuses its resolveUserLabel automatically so both plugins display users identically, and keeps its attribution fields out of the changed-fields noise.

Installation

pnpm add @whatworks/payload-activity-log

Usage

import { activityLogPlugin } from '@whatworks/payload-activity-log'
import { buildConfig } from 'payload'

export default buildConfig({
  // ...
  plugins: [
    // Register last so collections added by other plugins are logged too.
    activityLogPlugin(),
  ],
})

Then regenerate the import map so the admin panel can resolve the plugin's components:

payload generate:importmap

An Activity Log collection appears in the admin panel: a reverse-chronological feed with linked users, linked documents, the operation, and a link to the version diff for each change.

Options

activityLogPlugin({
  // Which collections to log. Defaults to all (the log collection itself is
  // always excluded).
  // - true — every collection (default)
  // - ['posts', 'pages'] — only these
  // - { exclude: ['redirects'] } — all except these
  collections: { exclude: ['redirects'] },

  // Which globals to log. Same semantics. Defaults to all.
  globals: true,

  // Per-event toggles (all default true except autosave).
  events: {
    create: true,
    update: true,
    trash: true, // moving to the trash (collections with trash: true)
    restore: true, // restoring from the trash
    delete: true, // permanent deletion
    login: true,
    logout: true,
    autosave: false, // default false — autosaves fire every few seconds while editing
  },

  // When to store a full JSON snapshot of the affected document:
  // 'delete' (default) — only on permanent delete, where it's the only surviving
  // record; 'always' — every change (watch your database size); 'never'.
  snapshot: 'delete',

  // Opt-in IP address tracking. When enabled, an `ipAddress` field is added to
  // the log collection and every entry stores the requester's address.
  // `true` reads the standard reverse-proxy headers (`cf-connecting-ip` →
  // `x-real-ip` → first `x-forwarded-for` entry); pass a function to resolve it
  // yourself when your proxy chain makes those untrustworthy. IP addresses are
  // personal data under most privacy regimes — consider pairing with `retention`.
  ipAddress: false,

  // Opt-in request host tracking. When enabled, a `requestHost` field is added
  // to the log collection and every entry stores the host the request was
  // addressed to. `true` reads the standard headers (`x-forwarded-host` →
  // `host`); pass a function to resolve it yourself when your proxy chain makes
  // those untrustworthy. Handy for multi-tenant / multi-domain deployments.
  requestHost: false,

  // Delete entries older than this. Off by default — nothing is pruned unless
  // you opt in. Pruning runs after log writes, at most once per hour.
  retention: { maxAgeDays: 90 },

  // Slug of the log collection. Default 'activity-log'.
  collectionSlug: 'activity-log',

  // Escape hatch for the generated collection — access control, labels, admin
  // options, extra fields.
  collectionOverride: (collection) => ({
    ...collection,
    access: { ...collection.access, read: ({ req }) => req.user?.role === 'admin' },
  }),

  // Auth collections whose users can appear as actors (and whose logins/logouts
  // are logged). Defaults to every auth-enabled collection in your config.
  userCollections: ['users'],

  // Custom actor resolution, e.g. attribute job/webhook writes to a bot user.
  // Return null/undefined to skip logging that event.
  resolveUser: ({ req }) =>
    req.user ? { relationTo: req.user.collection, value: req.user.id } : null,

  // Label stored for the acting user at event time. May be async.
  // Defaults to email → username → ID, or to audit-fields' resolveUserLabel
  // when that plugin is registered.
  resolveUserLabel: ({ user }) => user.email as string,

  // Title stored for the affected document at event time. May be async.
  // Defaults to the collection's useAsTitle value → title → name → email →
  // username → ID; for globals, the global's label.
  resolveDocumentLabel: ({ doc }) => doc.title as string,
})

Behaviour

  • Actors. Events are attributed to req.user. Writes without a user (seeds, migrations, job runners, local API calls without user) are not logged — pass resolveUser to attribute them to a dedicated system user instead.
  • What each entry stores. The actor (polymorphic reference + label), the operation, the affected collection/global and document ID, the document's title, the ID of the version the change produced (entities with versions), and the changed field names (updates). Labels and titles are captured at event time, so the feed stays readable after users or documents are deleted — at the cost of not reflecting later renames.
  • Trash. On collections with trash: true, moving to the trash logs trash (linked to the document under the trash route), restoring logs restore, and emptying the trash logs delete with a snapshot. Payload introduced trash in 3.49 — on older versions within the supported peer range these events simply never occur (there is no trash), and everything else works as described.
  • Autosave. Skipped by default; manual draft saves and publishes are logged. Set events.autosave: true to log every autosave.
  • IP addresses. Nothing is stored unless you opt in with ipAddress. When enabled, every entry (all operations, not just logins) records the requesting client's address; resolution failures simply leave it unset. Which proxy headers can be trusted depends on your deployment — pass a resolver function if the defaults pick up spoofable values.
  • Request host. Nothing is stored unless you opt in with requestHost. When enabled, every entry records the host the request was addressed to (x-forwarded-hosthost by default), which is useful for attributing activity across multi-tenant or multi-domain deployments. As with ipAddress, resolution failures leave it unset and forwarded headers are only as trustworthy as your proxy chain — pass a resolver function if the defaults pick up spoofable values.
  • The log is append-only. All mutating access on the log collection is disabled; entries are written exclusively by the plugin's hooks. Reads default to any authenticated user — tighten via collectionOverride. Note that stored labels/titles are visible to anyone who can read the log, regardless of their access to the source documents.
  • Log writes never break the operation that caused them: failures are logged and swallowed. Entries are written on the operation's request, so they commit and roll back with it.
  • The plugin does not touch Payload's internal collections (they are created after plugins run).

Versions link

Payload saves a change's version before afterChange hooks run, so each log entry can record the ID of the exact version its change produced. The Version column links straight to that version's diff view. For entities without versions the column shows , and the entry's changed-field names still tell you what was touched.

Cells only link to destinations that still resolve: once a document is permanently deleted (its versions are deleted with it) the version cell shows a muted "Document deleted" and the document cell shows the stored title without a link; a version pruned by versions.maxPerDoc shows . The actor gets the same treatment — a deleted user's label loses its link (in the list and on the entry view). Documents and users sitting in the trash link under the trash route. The existence checks are batched per render pass — one query per collection for a whole page of rows, not one per cell — and the actor checks share the same batch.

Using with @whatworks/payload-audit-fields

The two plugins are independent but designed to compose. Register both (order doesn't matter):

plugins: [auditFieldsPlugin(), activityLogPlugin()]
  • Audit fields answer "who touched this document" on the document itself and per-version; the activity log answers "what happened across the site", including deletes and logins that per-document views can't show.
  • The activity log reuses audit-fields' resolveUserLabel (unless you pass its own), so users display identically everywhere.
  • Audit-fields' createdBy/lastModifiedBy are excluded from changedFields.

Differences from @payload-bites/activity-log

This plugin is a rethink rather than a drop-in replacement (field names and stored shapes differ):

  • Logging is opt-out (all collections/globals by default) instead of a required opt-in map.
  • No data json copy of the whole document on every change — entries link to the version diff instead; snapshots only on permanent delete (configurable).
  • The feed shows document titles and user emails, not raw IDs, and they survive deletion.
  • Actors can belong to any auth collection, not only the admin user collection.
  • Local API writes are logged whenever a user is attached (or resolveUser returns one) — instead of being skipped wholesale.
  • Adds trash/restore classification, changed-field names, logins/logouts, and optional retention pruning.
  • Autosave logging defaults off (was on).
  • Read logging is not supported.