Skip to content

Conversation

@nickmeinhold
Copy link
Contributor

@nickmeinhold nickmeinhold commented Jan 28, 2026

Summary

This PR adds webhook functionality to notify external services when cards are created, updated, moved, or deleted. Webhooks are configured per-workspace through the Settings UI.

Configuration

Navigate to Settings → Webhooks to:

  • Add webhooks with custom name, URL, and secret
  • Select which events to subscribe to
  • Enable/disable webhooks without deleting them
  • Test webhooks with a single click

Webhook Payload

{
  "event": "card.created|card.updated|card.moved|card.deleted",
  "timestamp": "2024-01-29T10:30:00.000Z",
  "data": {
    "card": {
      "id": "123",
      "title": "Card title",
      "description": "Card description",
      "dueDate": "2024-02-01T00:00:00.000Z",
      "listId": "456",
      "boardId": "789"
    },
    "user": {
      "id": "user-id",
      "name": "User Name"
    },
    "changes": {
      "title": { "from": "Old Title", "to": "New Title" }
    }
  }
}

Headers

  • Content-Type: application/json
  • X-Webhook-Event: Event type
  • X-Webhook-Timestamp: ISO timestamp
  • X-Webhook-Signature: HMAC-SHA256 signature (if secret is configured)

Signature Verification

If a webhook secret is configured, the payload is signed with HMAC-SHA256:

const expectedSignature = crypto
  .createHmac('sha256', webhookSecret)
  .update(rawBody)
  .digest('hex');

if (signature !== expectedSignature) {
  throw new Error('Invalid signature');
}

Database Changes

New workspace_webhooks table:

  • id, publicId - identifiers
  • workspaceId - FK to workspace
  • name - user-friendly name
  • url - webhook endpoint URL
  • secret - optional HMAC secret
  • events - JSON array of subscribed events
  • active - enable/disable toggle
  • createdBy, createdAt, updatedAt - audit fields

Use Cases

  • Calendar sync (sync card due dates with Google Calendar)
  • Slack/Discord notifications
  • Third-party integrations
  • Audit logging

Test Coverage

  • 45 automated tests (unit + integration)
  • Integration tests use PGlite for real database testing

Test Plan

  • Create a webhook in Settings → Webhooks
  • Create a card - verify webhook is received
  • Update a card - verify webhook includes changes object
  • Move a card to another list - verify card.moved event
  • Delete a card - verify webhook is received
  • Test webhook button sends test payload
  • Verify HMAC signature when secret is set

🤖 Generated with Claude Code

nickmeinhold added a commit to 10xdeca/xdeca-infra that referenced this pull request Jan 28, 2026
- Mark OpenProject as stopped (replaced by Kan.bn)
- Update services table and Caddy routing
- Note calendar sync pending on webhook PR: kanbn/kan#343
- Update backup services list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@MaxwellMergeSlam MaxwellMergeSlam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review - PR #343

Summary: Adds optional webhook support for card events (create, update, move, delete) with HMAC signature verification.

Changes reviewed:

  • New packages/api/src/utils/webhook.ts - Webhook utility with signature generation
  • Modified packages/api/src/routers/card.ts - Added webhook calls to create/update/delete procedures
  • Modified apps/web/src/env.ts - Added WEBHOOK_URL and WEBHOOK_SECRET env vars

Quality Assessment:

Aspect Status Notes
Code Quality ✅ pass Clean, well-typed code following project patterns
Tests ⚠️ warning No tests added (but api package has no existing tests)
Security ✅ pass HMAC-SHA256 signature for payload verification
Performance ✅ pass Non-blocking (void + async), failures logged but don't block

Issues found:

  1. Minor - Missing timeout for fetch (packages/api/src/utils/webhook.ts:62)

    • The fetch call has no timeout, which could cause issues if the webhook endpoint is slow
    • Consider adding AbortController with a reasonable timeout (e.g., 5-10 seconds)
  2. Minor - Date serialization (packages/api/src/utils/webhook.ts:17)

    • dueDate is typed as Date | null but JSON.stringify will serialize it as an ISO string
    • The type in WebhookPayload should reflect this (string | null in the serialized form)
  3. Minor - Email exposure (packages/api/src/routers/card.ts:180,980)

    • User email is included in webhook payloads, which may be a privacy concern depending on the webhook consumer
    • Consider making email inclusion configurable or use a hashed identifier

Suggestions:

  1. Consider adding a retry mechanism for failed webhook deliveries (with exponential backoff)
  2. Could add support for multiple webhook URLs in the future
  3. Documentation in the README about the webhook feature would help users configure it

Verdict: APPROVE

This PR adds a clean, well-designed webhook feature that follows the project's patterns. The implementation is non-blocking and secure with HMAC signatures. The minor issues noted are suggestions for improvement rather than blocking concerns. The PR provides good value for enabling external integrations like calendar sync, notifications, and audit logging.

@nickmeinhold
Copy link
Contributor Author

Addressed the review feedback in commit 1f7d7ec:

  1. Timeout added - Added 10s timeout using AbortController to prevent hanging on slow webhook endpoints
  2. Date serialization fixed - Changed dueDate type to string | null to reflect JSON serialization, and explicitly call .toISOString()
  3. Email removed - Removed user email from webhook payloads for privacy

Thanks for the thorough review!

Copy link

@MaxwellMergeSlam MaxwellMergeSlam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review - PR #343

Summary: Adds webhook support for card events with full CRUD management UI, HMAC signature verification, and comprehensive test coverage.

Changes reviewed:

  • Database schema: workspace_webhooks table with RLS enabled
  • API router: CRUD operations + test endpoint with proper auth checks
  • Webhook utility: Fire-and-forget delivery with 10s timeout
  • UI: Settings page, webhook list, create/edit modal, delete confirmation
  • Tests: 45 tests (16 unit + 15 router + 14 integration with PGlite)
  • Localization: All 9 languages updated

Quality Assessment:

Aspect Status Notes
Code Quality ✅ pass Clean, well-structured code following existing patterns
Tests ✅ pass Excellent coverage - unit, router, and integration tests all pass
Security ✅ pass Admin-only access, HMAC-SHA256 signatures, input validation
Performance ✅ pass Fire-and-forget delivery, 10s timeout prevents blocking

Highlights:

  • Proper authorization: All endpoints require admin role via assertUserInWorkspace
  • Webhook isolation: Webhooks are workspace-scoped with FK constraints
  • Robust delivery: Promise.allSettled ensures one failure doesn't affect others
  • Good UX: Test button, active/inactive toggle, secret field handling

Minor observations (non-blocking):

  • env.ts adds WEBHOOK_URL and WEBHOOK_SECRET which appear unused (webhooks are now per-workspace)
  • Consider adding retry logic for failed deliveries in future iteration

Verdict: APPROVE

This is a well-implemented feature with solid test coverage and proper security controls. The code follows existing patterns in the codebase and is ready to merge.

nickmeinhold and others added 17 commits February 13, 2026 00:24
Add optional webhook functionality to notify external services
when cards are created, updated, moved, or deleted.

Configuration:
- WEBHOOK_URL: URL to POST webhook payloads to
- WEBHOOK_SECRET: Optional secret for HMAC signature verification

Webhook payload includes:
- Event type (card.created, card.updated, card.moved, card.deleted)
- Card data (id, title, description, dueDate, listId, boardId)
- User who performed the action
- Changes object (for updates) showing old/new values

The webhook is fire-and-forget (non-blocking) to avoid
impacting API response times. Failures are logged but don't
affect the main operation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 10s timeout to webhook fetch using AbortController
- Fix dueDate type to string (reflects JSON serialization)
- Remove email from user payload for privacy

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add database schema for per-workspace webhook configuration:
- workspaceWebhooks table with url, secret, events, active fields
- Foreign keys to workspace and user tables
- Row-level security enabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add repository functions for workspace webhooks:
- create, update, getByPublicId, hardDelete
- getAllByWorkspaceId for listing
- getActiveByWorkspaceId for sending webhooks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new webhook utility functions:
- sendWebhookToUrl: Send to specific URL with result
- sendWebhooksForWorkspace: Query DB and send to all active webhooks
- Keep legacy sendWebhook for backwards compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add webhook router with endpoints:
- list: Get all webhooks for workspace
- create: Add new webhook
- update: Modify webhook settings
- delete: Remove webhook
- test: Send test payload

All endpoints require admin role.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update card create/update/delete to send webhooks to all
active workspace webhooks instead of just env var webhook.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add React components for webhook management:
- WebhookList: Table with edit/test/delete actions
- NewWebhookModal: Create/edit form with event checkboxes
- DeleteWebhookConfirmation: Confirmation dialog

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add WebhookSettings page with modals
- Add webhooks.tsx page route
- Add Webhooks tab to settings navigation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove WEBHOOK_URL/WEBHOOK_SECRET env var fallback.
Webhooks are now only configured via the database/UI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive test coverage for webhook functionality:
- webhook.test.ts: Tests for createCardWebhookPayload, sendWebhookToUrl,
  and sendWebhooksForWorkspace utility functions
- webhook.test.ts (router): Tests for list, create, update, delete, and
  test procedures including authorization checks

Also adds vitest to @kan/api package for running tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add integration tests that use a real in-memory PGlite database:
- test-db.ts: Helper to create fresh PGlite instances with migrations
- webhook.integration.test.ts: 14 tests covering all webhook repo CRUD ops

Integration tests verify actual database behavior vs mocked unit tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reset locale files to main and recompile. Webhook translation strings
will use English fallback until properly extracted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add name fields to existing Drizzle queries in list and card repos,
then pass them through to createCardWebhookPayload at all three
webhook call sites (create, update/move, delete).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants