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

v0.1.1

Published

Payload plugin that tracks who created and last modified every document, with an enhanced versions view showing who modified each version.

Readme

@whatworks/payload-audit-fields

Payload plugin that records who created and who last modified every document, and upgrades the versions list view with a Modified By column so you can see exactly who saved each version.

  • Adds read-only createdBy / lastModifiedBy relationship fields to every collection and global by default (opt-out, not opt-in).
  • Displays attributed users by email (customizable via resolveUserLabel) — both on the document and in the versions view — linked to the user document.
  • Replaces the versions list view with a faithful recreation of Payload's own view, extended with a per-version "Modified By" column.
  • Supports multiple auth collections — attribution is stored as a polymorphic reference to whichever collection the acting user belongs to.
  • Data-compatible with @payload-bites/audit-fields — same default field names and the same stored value shape ({ relationTo, value }), so you can switch plugins without a migration.

Installation

pnpm add @whatworks/payload-audit-fields

Usage

import { auditFieldsPlugin } from '@whatworks/payload-audit-fields'
import { buildConfig } from 'payload'

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

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

payload generate:importmap

That's it — every collection and global now tracks createdBy and lastModifiedBy, and every entity with versions enabled gets the enhanced versions view.

Options

auditFieldsPlugin({
  // Which collections to audit. Defaults to all.
  // - true — every collection (default)
  // - ['posts', 'pages'] — only these
  // - { exclude: ['tags'] } — all except these
  collections: { exclude: ['tags'] },

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

  // Auth collections users can be attributed to.
  // Defaults to every auth-enabled collection in your config.
  userCollections: ['users'],

  fields: {
    createdBy: {
      name: 'createdBy',
      label: 'Created By', // string | { [locale]: string } | (slug) => label
      // Escape hatch for full control over the generated field, e.g. delete
      // `field.admin.components` to restore the default relationship input:
      override: (field) => ({ ...field, index: true }),
    },
    // Set to false to only track one of the two fields:
    lastModifiedBy: { name: 'lastModifiedBy', label: 'Last Modified By' },
  },

  showInSidebar: false, // default false — fields render at the end of the main field area
  index: false, // default false — add a DB index to the fields

  // Custom attribution, e.g. attribute job/webhook writes to a bot user.
  // Return null/undefined to leave the fields untouched for that change.
  resolveUser: ({ req }) =>
    req.user ? { relationTo: req.user.collection, value: req.user.id } : null,

  // How attributed users are displayed — on the document and in the versions
  // view. May be async. Defaults to email → username → ID.
  resolveUserLabel: ({ user }) =>
    user.firstName ? `${user.firstName} ${user.lastName}` : (user.email as string),

  // The enhanced versions list view. Default: true.
  versionsView: {
    columnLabel: 'Modified By', // string | { [locale]: string }
  },
  // ...or `versionsView: false` to keep Payload's default view.
})

Behaviour

  • Create sets both createdBy and lastModifiedBy to the acting user.
  • Update sets lastModifiedBy and strips any incoming createdBy value, so API clients cannot rewrite attribution after the fact.
  • Writes without a user (seeds, migrations, job runners, local API calls without user/req) leave both fields untouched, so scripts can set them explicitly when needed — or use resolveUser to attribute them to a dedicated system user.
  • If an entity already defines a field with one of the configured names, the plugin leaves that entity's field and its values entirely alone.
  • The plugin does not touch Payload's internal collections (payload-preferences, payload-migrations, payload-locked-documents, payload-folders, …) — they are created after plugins run, are system-managed, or already carry their own user reference.

How users are displayed

On the document, audit fields render as a read-only label linking to the user document instead of the default (greyed-out) relationship input. In the versions view, the "Modified By" column shows the same label.

Both use resolveUserLabel to derive the label from the user document, which is fetched with the viewing user's access rights — if the current user cannot read the users collection, the raw ID is shown instead. The default resolver returns user.email, falling back to user.username, then the ID.

resolveUserLabel may be async, so you can derive the label from anywhere:

resolveUserLabel: async ({ relationTo, req, user }) => {
  const profile = await req.payload.findByID({ collection: 'profiles', id: user.profile })
  return profile?.displayName ?? (user.email as string)
}

Versions view

For every audited entity with versions enabled, the plugin sets admin.components.views.edit.versions to its own server view (@whatworks/payload-audit-fields/rsc#AuditVersionsView) — unless you have already customized that view, in which case yours wins.

The view is a faithful recreation of Payload's built-in versions list (same layout, pagination, status pills, autosave badges, and trash support), with one addition: a Modified By column showing who saved each version, linked to the user document. It reads lastModifiedBy from each version snapshot and falls back to createdBy for the initial version.

The vendored view internals come from [email protected] and degrade gracefully on the older Payload versions in the peer range (version-gated props are simply absent there); if Payload materially changes its versions view, update the vendored files alongside the peer bump.

Migrating from @payload-bites/audit-fields

Field names, labels, and the stored value shape are identical by default, so swapping plugins requires no data migration. Differences to be aware of:

  • lastModifiedBy is now also set on create (previously only on update).
  • createdBy can no longer be overwritten through authenticated API updates.
  • Audit fields render at the end of the main field area by default; pass showInSidebar: true for the old placement.
  • Fields display the user's email as a link instead of a disabled relationship input (customize via resolveUserLabel, or restore the input via fields.createdBy.override).
  • excludedCollections / excludedGlobals become collections: { exclude: [...] } / globals: { exclude: [...] }.
  • Attribution uses req.user.collection, so users from any auth collection are recorded correctly (previously always the admin user collection).