Files
at-container-registry/docs/WEBHOOKS.md
2026-02-28 14:42:35 -06:00

6.8 KiB

Webhooks

Webhooks notify external services when events occur in the registry. Payloads are JSON, signed with HMAC-SHA256 (optional), and delivered with retry (exponential backoff: 0s, 30s, 2m, 8m). Discord and Slack URLs are auto-detected and receive platform-native formatting.

Current Events

push — Image Push

Fires when a manifest is stored (the "logical push complete" moment). Tagless pushes (e.g., buildx platform manifests) also fire with an empty tag field.

Bitmask: 0x08 — Free tier

{
  "trigger": "push",
  "push_data": {
    "pushed_at": "2026-02-27T15:30:00Z",
    "pusher": "alice.bsky.social",
    "pusher_did": "did:plc:abc123",
    "tag": "latest",
    "digest": "sha256:abc..."
  },
  "repository": {
    "name": "myapp",
    "namespace": "alice.bsky.social",
    "repo_name": "alice.bsky.social/myapp",
    "repo_url": "https://buoy.cr/alice.bsky.social/myapp",
    "media_type": "application/vnd.oci.image.manifest.v1+json",
    "star_count": 42,
    "pull_count": 1337
  },
  "hold": {
    "did": "did:web:hold01.atcr.io",
    "endpoint": "https://hold01.atcr.io"
  }
}

repo_url uses registry_domains[0] (the pull domain) when configured, otherwise falls back to base_url.

scan:first — First Scan

Fires the first time an image is scanned (no previous scan record exists).

Bitmask: 0x01 — Free tier

scan:all — Every Scan

Fires on every scan completion.

Bitmask: 0x02 — Paid tier

scan:changed — Vulnerability Change

Fires when vulnerability counts change from the previous scan. Includes a previous field with the old counts.

Bitmask: 0x04 — Paid tier

Scan payload format (shared by all scan triggers):

{
  "trigger": "scan:first",
  "holdDid": "did:web:hold01.atcr.io",
  "holdEndpoint": "https://hold01.atcr.io",
  "manifest": {
    "digest": "sha256:abc...",
    "repository": "myapp",
    "tag": "latest",
    "userDid": "did:plc:abc123",
    "userHandle": "alice.bsky.social"
  },
  "scan": {
    "scannedAt": "2026-02-27T16:00:00Z",
    "scannerVersion": "atcr-scanner-v1.0.0",
    "vulnerabilities": {
      "critical": 0,
      "high": 2,
      "medium": 5,
      "low": 12,
      "total": 19
    }
  },
  "previous": null
}

For scan:changed, the previous field contains the previous vulnerability counts.

Billing

Tier Max Webhooks Available Triggers
Free 1 push, scan:first
Paid Per plan All triggers
Captain Unlimited All triggers

Free users can enable both push and scan:first on their single webhook.

Security

  • HMAC-SHA256 signing: If a secret is set, payloads include X-Webhook-Signature-256: sha256=<hex>. The signature covers the delivered payload (including platform-specific formatting for Discord/Slack).
  • Retry: 4 attempts with exponential backoff (0s, 30s, 2m, 8m).
  • Test delivery: The settings UI supports sending a test payload to verify connectivity.

Implementation

  • Types: pkg/appview/webhooks/types.go
  • Dispatch + retry: pkg/appview/webhooks/dispatch.go
  • Discord/Slack formatting: pkg/appview/webhooks/format.go
  • UI handlers: pkg/appview/handlers/webhooks.go
  • Settings page SSR: pkg/appview/handlers/settings.go
  • Template: pkg/appview/templates/partials/webhooks_list.html
  • Trigger bitmask stored in webhooks.triggers column (integer)

Future Events

Inspired by Harbor's webhook model. These are not yet implemented but document the intended direction.

pull — Image Pull

Bitmask: 0x10 (reserved)

Fires when a manifest is pulled. This is tricky because pulls go through presigned S3 URLs — the appview issues a redirect and never sees the actual blob download. Manifest fetches are visible to the appview, so a pull event would fire on manifest GET, not blob download.

Scalability concern: Public repos with high pull volume would generate excessive webhook traffic. Would need rate limiting or batching (e.g., "5 pulls in the last minute" digest). Not suitable for free tier without throttling.

Suggested payload:

{
  "trigger": "pull",
  "pull_data": {
    "pulled_at": "2026-02-27T15:30:00Z",
    "puller": "bob.bsky.social",
    "puller_did": "did:plc:def456",
    "tag": "latest",
    "digest": "sha256:abc..."
  },
  "repository": {
    "name": "myapp",
    "namespace": "alice.bsky.social",
    "repo_name": "alice.bsky.social/myapp",
    "repo_url": "https://buoy.cr/alice.bsky.social/myapp",
    "star_count": 42,
    "pull_count": 1338
  },
  "hold": {
    "did": "did:web:hold01.atcr.io",
    "endpoint": "https://hold01.atcr.io"
  }
}

Anonymous pulls would have empty puller / puller_did fields.

delete — Manifest Delete

Bitmask: 0x20 (reserved)

Fires when a manifest is deleted from the user's PDS. Lower priority — deletes are uncommon.

Suggested payload:

{
  "trigger": "delete",
  "delete_data": {
    "deleted_at": "2026-02-27T15:30:00Z",
    "deleted_by": "alice.bsky.social",
    "deleted_by_did": "did:plc:abc123",
    "tag": "v1.0.0",
    "digest": "sha256:abc..."
  },
  "repository": {
    "name": "myapp",
    "namespace": "alice.bsky.social",
    "repo_name": "alice.bsky.social/myapp",
    "repo_url": "https://buoy.cr/alice.bsky.social/myapp",
    "star_count": 42,
    "pull_count": 1337
  }
}

No hold field — deletion removes the manifest record from the PDS; blob cleanup is handled separately by GC.

quota:warning / quota:exceeded — Storage Quota

Bitmask: 0x40 (warning), 0x80 (exceeded) — reserved

Fires when a hold's storage quota reaches a threshold or is exceeded. Open design questions:

  • Thresholds: Harbor uses a single warning threshold (85%). Options: fixed 80/90/100%, or configurable per hold.
  • Recipient: Who gets the webhook — the user who pushed (triggering the quota check), the hold captain, or both? Likely the captain, since they own the storage.
  • Scope: Per-user quotas (crew member limits) vs per-hold quotas (total storage). Both exist in the quota system.

Suggested payload:

{
  "trigger": "quota:warning",
  "quota_data": {
    "timestamp": "2026-02-27T15:30:00Z",
    "usage_bytes": 8589934592,
    "limit_bytes": 10737418240,
    "usage_percent": 80,
    "threshold_percent": 80
  },
  "hold": {
    "did": "did:web:hold01.atcr.io",
    "endpoint": "https://hold01.atcr.io"
  },
  "user": {
    "did": "did:plc:abc123",
    "handle": "alice.bsky.social"
  }
}

Events explicitly not planned

  • Scan failed / scan stopped — Server-side operational issues, not user-actionable. Belongs in ops monitoring (logs, alerting), not user-facing webhooks.
  • Replication — No replication feature in ATCR.
  • Tag retention — No retention policies yet.