# 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 ```json { "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): ```json { "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=`. 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](https://goharbor.io/docs/working-with-projects/project-configuration/configure-webhooks/). 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:** ```json { "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:** ```json { "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:** ```json { "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.