Webhooks and event notifications

Organizations often need notifications when registry events occur, such as when a new package version is published, dist-tags change, or user permissions are modified.

This guide explains pnpr's event architecture and shows how to integrate external webhook dispatchers and notification pipelines.

Architecture decision#

pnpr deliberately does not maintain an in-process HTTP webhook dispatch queue.

Building an internal webhook queue inside a registry server introduces operational drawbacks:

  • Delivery guarantees: An in-memory queue inside the server loses pending webhooks if an instance crashes or restarts during a deployment. Storing persistent queue state on local disk complicates running multiple stateless pnpr replicas behind a load balancer.
  • Process stability and head-of-line blocking: Slow or unreachable webhook destination endpoints can cause queue buildup, memory pressure, and retry storms inside the registry process, potentially degrading core package download and publish throughput.
  • Separation of concerns: Reliable event delivery, deduplication, retry policies, and fan-out are best handled by dedicated event streaming and log shipping systems.

Instead, pnpr provides durable, structured integration points designed to feed external webhook dispatchers and message brokers.

Integration patterns#

1. Structured audit logs#

Administrative and destructive operations in pnpr emit a structured event under the pnpr::audit tracing target at info level. They cover account, team, rule, and SCIM changes, token revocation, unpublishes, packument and maintainer updates, dist-tag changes, and image deletions. Publishes and token creation are not audited. The full list of actions and their fields is in Admin audit logs.

When configured with log.format: json, pnpr outputs each audit event as a single-line JSON record on stdout:

{
  "timestamp": "2026-10-17T01:15:30.123456Z",
  "level": "INFO",
  "target": "pnpr::audit",
  "fields": {
    "action": "package.dist_tag_set",
    "actor": "alice",
    "package": "@mycompany/utils",
    "tag": "latest",
    "version": "1.2.0",
    "registry": "local",
    "message": "package dist-tag set"
  },
  "span": {
    "name": "request",
    "request_id": "c8a14b09-1a73-42e1-95be-726ff342f534",
    "client_ip": "192.168.1.50"
  }
}

Lightweight log shippers such as Vector, Fluent Bit, or Fluentd can filter for target == "pnpr::audit" and forward these events to an HTTP webhook endpoint, Slack, or a message broker (such as Kafka, RabbitMQ, or AWS SQS).

Example: Forwarding audit logs to a webhook with Vector#

Here is a Vector configuration that extracts pnpr::audit events from pnpr's container logs and forwards them to a webhook URL:

vector.toml
[sources.pnpr_logs]
type = "docker_logs"
include_containers = ["pnpr"]

[transforms.parse_pnpr_logs]
type = "remap"
inputs = ["pnpr_logs"]
source = ". = parse_json!(.message)"

[transforms.filter_audit_events]
type = "filter"
inputs = ["parse_pnpr_logs"]
condition = '.target == "pnpr::audit"'

[sinks.webhook_dispatcher]
type = "http"
inputs = ["filter_audit_events"]
uri = "https://events.example.com/webhooks/registry"
method = "post"
encoding.codec = "json"

[sinks.webhook_dispatcher.request.headers]
Authorization = "Bearer ${WEBHOOK_SECRET}"
Content-Type = "application/json"

2. Object storage bucket notifications#

When pnpr is configured with an S3-compatible backend (AWS S3, Cloudflare R2, MinIO), you can configure bucket notifications directly on your object storage bucket.

Package tarballs are stored as immutable blobs under the bucket prefix, so storage notifications on new tarballs are durable, real-time blob events. They are not publish events. pnpr uploads a tarball before it commits the metadata that references it, so a notification can arrive before the package is visible, and an interrupted publish can leave a tarball that no package references. A consumer verifies the version in the package metadata, retrying for a while, before acting. A consumer that needs publish events uses the publish response or a CI post-publish step instead. Package metadata lives in the metadata database, so a dist-tag move or a deprecation creates no object:

  • AWS S3: Configure an EventBridge or SNS/SQS notification for s3:ObjectCreated:* events matching the prefix of hosted packages. An AWS Lambda function or worker consumes the event and dispatches your webhook.
  • MinIO: Use MinIO bucket event notifications to dispatch events directly to an HTTP webhook endpoint.
  • Cloudflare R2: Use R2 Event Notifications connected to a Cloudflare Worker or queue to invoke downstream webhooks.

3. CI/CD post-publish steps#

For publishing workflows orchestrated by continuous integration (such as GitHub Actions or GitLab CI), invoking notification webhooks immediately after pnpm publish provides immediate, synchronous confirmation without requiring server-side webhooks:

.github/workflows/release.yml
- name: Publish to pnpr
  run: pnpm publish --registry https://registry.example.com/npm/

- name: Dispatch notification webhook
  if: success()
  run: |
    curl -X POST https://events.example.com/webhooks/published \
      -H "Content-Type: application/json" \
      -d '{"package": "${{ github.repository }}", "ref": "${{ github.ref_name }}"}'