SQL metadata is now pnpr's only metadata backend: SQLite in the storage directory by default, or the database `metadata.url` names. - `MetadataBackend::Documents` and the documents implementation are removed. A config that still sets `metadata.backend` is refused with a pointer at `metadata.url`, and an `s3:` store needs a libSQL `metadata.url`. - The publish journal goes with it, since only the documents backend committed through it. Startup recovery becomes `prepare_storage`, and a commit that loses a blob slot fails as a whole. - With one implementation left, the `PackageMetadata` trait becomes a struct and its documents-only defaults go. `Storage::new` takes the metadata backend, and `sql_metadata` always returns the database. - A tier that hosts no packages needs no metadata URL with `s3:`. - `migrate-storage` moves archives only. The metadata stays in the configured database, which a real run requires to be libSQL. - `oci-gc` also checks the manifests of a repository whose metadata has no blob beside it. pnpr is pre-0, so a store that kept documents starts with empty metadata. Nothing reads or migrates them.
5.4 KiB
id, title
| id | title |
|---|---|
| webhooks | 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:
[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:
- 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 }}"}'