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

@trieb.work/payload-audit

v1.3.0

Published

Payload CMS plugin for automatic, compliance-ready audit logging across all collections

Readme

@trieb.work/payload-audit

npm version CI License: MIT Payload CMS

Automatic, compliance-ready audit logging and audit trail plugin for Payload CMS. Zero-config change tracking with delegation, forensics, retention, and multi-tenancy built in.

Manually wiring up audit logs for every collection is tedious and easy to get wrong — one forgotten hook and your audit trail has a gap. payload-audit attaches itself to every collection in your Payload config automatically and writes an immutable log entry for every create, update, and delete, so you get a complete, tamper-proof activity trail with a single line of configuration.

It's built to help satisfy the logging and accountability requirements of NIS-2, CRA, GDPR, SEC Cyber Disclosure Rules, HIPAA, PCI-DSS 4.0, ISO/IEC 27001, and SOC 2 — but it's just as useful as a general-purpose "who changed what, when" activity log for any Payload project.

Features

  • Zero-config coverage — hooks into every collection automatically, with an opt-out list (disabledCollections). No per-collection setup.
  • Auth events — native Payload login/logout/lockout are recorded automatically; external auth (passwordless, Better Auth, …) uses a one-line emitAuthEvent call.
  • Immutable audit trail — the generated audit-logs collection denies create/update/delete through the API; entries can only be written internally by the plugin, so the log can't be altered or deleted by users.
  • Rich context per entry — actor (with email/name snapshot that survives user deletion), document id and title, IP address, and user agent.
  • File tracking — upload-enabled collections get dedicated file_upload / file_delete actions.
  • Delegation & impersonation aware (RFC 8693 act semantics) — records who performed an action on behalf of whom, including nested delegation chains (User → Agent → Service), via onBehalfOf and delegationChain.
  • Forensic metadata for breach investigation — optionally capture auth strategy, HTTP method, request path, and a non-reversible token fingerprint (sha256) to correlate every action performed with a stolen credential, without ever storing the raw token.
  • Custom actions — extend the built-in action types with your own (extraActions), e.g. impersonation.started, for manually emitted entries.
  • Configurable retention — prune entries by age (maxAge), by count (maxEntries), or both, via a scheduled Payload job (custom cron/queue supported, or trigger manually).
  • Multi-tenant scoping — optional tenant relationship on every entry, with auto-detection and out-of-the-box interop with @payloadcms/plugin-multi-tenant.
  • Configurable read access — lock down who can view the audit trail via a standard Payload access.read function.
  • Programmatic APIwriteAuditLog, resolveDelegation, extractTenant, resolveDocTitle, and pruneAuditLogs are all exported for custom hooks and scripts. A skipAuditLog request-context flag lets you suppress logging for a single operation when you emit a more specific entry yourself.

Quick start

pnpm add @trieb.work/payload-audit
// payload.config.ts
import { auditLogPlugin } from '@trieb.work/payload-audit'

export default buildConfig({
  // ...
  plugins: [
    auditLogPlugin({
      disabledCollections: ['sessions'],
      retention: { maxAge: 365 }, // keep one year of history
    }),
  ],
})

That's it — every collection now writes to an audit-logs collection automatically. Open the admin UI to see the trail.

Configuration

All options are optional; sensible, safe defaults are used when omitted.

auditLogPlugin({
  // Master switch. `false` turns the plugin into a no-op.
  enabled: true,

  // Slug of the generated audit collection. Default: 'audit-logs'.
  collectionSlug: 'audit-logs',

  // Collections that should never be audited (in addition to the audit
  // collection itself and Payload's internal collections).
  disabledCollections: ['sessions'],

  // Read access for the audit trail. Default: any authenticated user.
  access: { read: ({ req }) => req.user?.role === 'admin' },

  // Prune old entries by age and/or count via a scheduled job.
  retention: { maxAge: 365, maxEntries: 100_000, cron: '0 0 * * *' },

  // Scope entries per tenant. Interoperates with @payloadcms/plugin-multi-tenant.
  multiTenant: { enabled: true, autoDetect: true },

  // Opt-in forensic metadata for breach investigation.
  forensics: {
    authStrategy: true,
    requestMethod: true,
    tokenFingerprint: true,
  },

  // RFC 8693 delegation/impersonation-aware logging. Enabled by default.
  delegation: { enabled: true, maxChainDepth: 10 },

  // Custom action types beyond create/update/delete/file_upload/file_delete.
  extraActions: [
    { value: 'impersonation.started', label: 'Impersonation started' },
  ],
})

Authentication events

Native Payload auth (email/password, API keys, anything that goes through payload.login() / logout() / refresh()) is recorded automatically:

  • auth.login.success / auth.login.failure
  • auth.logout
  • auth.account.locked (when maxLoginAttempts is enabled)
  • auth.token.refresh and auth.password.forgot are off by default (noisy / PII). Enable them via authEvents.events.
auditLogPlugin({
  authEvents: {
    events: { refresh: true },
    captureIdentifier: 'known-user', // default: store email only if the account exists
  },
})

Internal user-document writes during login (sessions, loginAttempts) are not logged as update.

Failed login and lockout are recorded via Payload's afterError hook, which runs for REST and GraphQL. Local API payload.login() rethrows without that hook, so those paths are covered by HTTP-level tests.

External auth (emitAuthEvent)

Plugins that replace Payload's login (passwordless, Better Auth, custom endpoints) never fire afterLogin. Call emitAuthEvent at the login / logout / failure site — the plugin already stored its options on payload.config.custom, so the call is one argument besides req:

import { emitAuthEvent } from '@trieb.work/payload-audit'

await emitAuthEvent({ req, event: 'login.success', user })
await emitAuthEvent({ req, event: 'login.failure', identifier: email })
await emitAuthEvent({ req, event: 'logout', user })

Register the auth plugin first, then auditLogPlugin, then attach hooks to collections the auth plugin created (e.g. sessions):

import type { Plugin } from 'payload'
import { emitAuthEvent } from '@trieb.work/payload-audit'

plugins: [authPlugin(), auditLogPlugin(), attachSessionAuthAudit('sessions')]

function attachSessionAuthAudit(slug: string): Plugin {
  return (config) => {
    const collection = config.collections?.find((c) => c.slug === slug)
    if (!collection) return config
    collection.hooks ??= {}
    collection.hooks.afterChange = [
      ...(collection.hooks.afterChange ?? []),
      async ({ doc, operation, req }) => {
        if (operation === 'create') {
          const user =
            typeof doc.user === 'object' ? doc.user : { id: doc.user }
          await emitAuthEvent({ req, event: 'login.success', user })
        }
        return doc
      },
    ]
    collection.hooks.afterDelete = [
      ...(collection.hooks.afterDelete ?? []),
      async ({ doc, req }) => {
        const user =
          typeof doc?.user === 'object' ? doc.user : { id: doc?.user }
        await emitAuthEvent({ req, event: 'logout', user })
        return doc
      },
    ]
    return config
  }
}

The same snippet works for Better Auth (payload-auth): use the session collection slug that plugin registers (sessions by default).

See the exported TypeScript types (AuditLogPluginConfig and friends) for the full reference and inline documentation.

Compliance mapping

| Requirement | Covered by | | ------------------------------------------------------------ | ------------------------------------------------------------ | | Who changed what, when (GDPR Art. 30, SOC 2 CC7) | Automatic actor/action/timestamp capture on every collection | | Tamper-proof records (ISO 27001 A.8.15, PCI-DSS 4.0 10.3) | Immutable collection, API writes always denied | | Incident detection & forensics (NIS-2, SEC Cyber Disclosure) | Forensics metadata, token fingerprinting, delegation chains | | Data retention limits (GDPR storage limitation) | Configurable retention (age/count) with scheduled pruning | | Access accountability across tenants (HIPAA, SOC 2) | Multi-tenant scoping of the audit trail |

This is a starting point, not legal advice — always validate against your own compliance obligations.

Development

pnpm install
pnpm dev          # start the dev Payload app (zero-config, in-memory Mongo)
pnpm build        # build the publishable plugin

Testing

The test suite is split into three layers:

  • Unit tests (pnpm test:int) — Fast, isolated tests for helpers (extractRequestMeta, resolveDocTitle, extractTenant) and plugin config wiring. Uses Vitest with vite-tsconfig-paths.

  • Integration tests (pnpm test:int) — Same Vitest run, but tests live against a real Payload instance (via getPayload with the dev config and mongodb-memory-server). Covers create/update/delete logging, upload tracking, multi-tenant scoping, retention pruning, immutability, native login events, and emitAuthEvent against real @trieb.work/payload-auth-pwless and payload-auth (Better Auth).

  • E2E tests (pnpm test:e2e) — Playwright tests against the running admin UI. Requires a built dev app first:

    pnpm build:dev   # or start the dev server manually
    pnpm test:e2e

Run everything:

pnpm test          # test:int + test:e2e

Contributing

Issues and pull requests are welcome — see the GitHub repository.

License

MIT © trieb.work