Docs /features/webhooks

Webhooks

Event-driven integrations and notifications (Planned Feature)

๐Ÿ“‹ Overview

Webhooks enable real-time notifications to external systems when events occur in Izri, such as test runs completing, analyses finishing, or project updates.

๐ŸŽฏ Current Status

Status: ๐Ÿ“‹ Planned (Phase 4)

Webhook infrastructure is not yet implemented. This is planned alongside CI/CD integration features.

๐Ÿ—๏ธ Planned Architecture

Database Schema

webhooks table:

{
  id: string
  projectId: string?        // Project-specific or org-wide
  organizationId: string?
  url: string               // Webhook endpoint URL
  events: string[]          // Subscribed events
  secret: string            // For signature verification
  active: boolean
  createdAt: Date
  updatedAt: Date
}

webhookDeliveries table:

{
  id: string
  webhookId: string
  event: string
  payload: object           // Event data
  requestHeaders: object
  responseStatus: number?
  responseBody: string?
  attemptCount: number
  deliveredAt: Date?
  failedAt: Date?
  nextRetryAt: Date?
  createdAt: Date
}

๐Ÿ“ก Supported Events (Planned)

Test Events

// test_run.started
{
  event: 'test_run.started',
  data: {
    runId: 'run_123',
    projectId: 'proj_456',
    type: 'unit',
    startedAt: '2024-01-01T00:00:00Z'
  }
}

// test_run.completed
{
  event: 'test_run.completed',
  data: {
    runId: 'run_123',
    projectId: 'proj_456',
    status: 'success',
    results: {
      passed: 50,
      failed: 2,
      duration: 15000,
      coverage: 85.5
    }
  }
}

// test_run.failed
{
  event: 'test_run.failed',
  data: {
    runId: 'run_123',
    projectId: 'proj_456',
    error: 'Test execution timeout'
  }
}

Analysis Events

// analysis.completed
{
  event: 'analysis.completed',
  data: {
    analysisId: 'analysis_789',
    projectId: 'proj_456',
    commitSha: 'abc123...',
    testCandidates: 45
  }
}

// analysis.failed
{
  event: 'analysis.failed',
  data: {
    projectId: 'proj_456',
    error: 'Repository not accessible'
  }
}

Project Events

// project.created
{
  event: 'project.created',
  data: {
    projectId: 'proj_456',
    name: 'My Project',
    repository: 'https://github.com/user/repo'
  }
}

// project.updated
// project.deleted

๐Ÿ”Œ API Reference (Planned)

Create Webhook

const webhook = await trpc.webhooks.create.mutate({
  projectId: 'proj_123',
  url: 'https://api.example.com/webhooks/izri',
  events: ['test_run.completed', 'test_run.failed'],
  secret: 'webhook_secret_here'
})

// Response
{
  webhook: {
    id: 'webhook_456',
    url: 'https://api.example.com/webhooks/izri',
    events: ['test_run.completed', 'test_run.failed'],
    active: true,
    createdAt: '2024-01-01T00:00:00Z'
  }
}

List Webhooks

const webhooks = await trpc.webhooks.list.query({
  projectId: 'proj_123'
})

Update Webhook

await trpc.webhooks.update.mutate({
  webhookId: 'webhook_456',
  events: ['test_run.completed'], // Change subscriptions
  active: false // Disable temporarily
})

Delete Webhook

await trpc.webhooks.delete.mutate({
  webhookId: 'webhook_456'
})

Test Webhook

// Send test ping
await trpc.webhooks.test.mutate({
  webhookId: 'webhook_456'
})

// Sends:
{
  event: 'ping',
  data: {
    webhookId: 'webhook_456',
    timestamp: '2024-01-01T00:00:00Z'
  }
}

List Deliveries

const deliveries = await trpc.webhooks.listDeliveries.query({
  webhookId: 'webhook_456',
  limit: 50
})

// Response
{
  deliveries: [
    {
      id: 'delivery_789',
      event: 'test_run.completed',
      responseStatus: 200,
      deliveredAt: '2024-01-01T00:00:00Z',
      attemptCount: 1
    }
  ]
}

Redeliver Webhook

// Retry failed delivery
await trpc.webhooks.redeliver.mutate({
  deliveryId: 'delivery_789'
})

๐Ÿ”’ Security

Signature Verification

Request headers:

X-Izri-Signature: sha256=abc123...
X-Izri-Event: test_run.completed
X-Izri-Delivery: delivery_789

Verification (in your webhook receiver):

import crypto from 'node:crypto'

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const hmac = crypto.createHmac('sha256', secret)
  const digest = 'sha256=' + hmac.update(payload).digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  )
}

// Express/Hono handler
app.post('/webhooks/izri', async (c) => {
  const signature = c.req.header('x-izri-signature')
  const payload = await c.req.text()
  
  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    return c.json({ error: 'Invalid signature' }, 401)
  }
  
  const event = JSON.parse(payload)
  // Process event...
  
  return c.json({ received: true })
})

๐Ÿ”„ Retry Logic (Planned)

Retry Strategy

const RETRY_DELAYS = [
  1000,      // 1 second
  5000,      // 5 seconds
  30000,     // 30 seconds
  300000,    // 5 minutes
  1800000    // 30 minutes
]

async function deliverWebhook(webhook: Webhook, event: Event) {
  let attemptCount = 0
  
  while (attemptCount < RETRY_DELAYS.length) {
    try {
      const response = await sendWebhook(webhook, event)
      
      if (response.status >= 200 && response.status < 300) {
        // Success
        await recordSuccess(webhook, event, response)
        return
      }
      
      // Non-2xx response, retry
      attemptCount++
      await sleep(RETRY_DELAYS[attemptCount - 1])
      
    } catch (error) {
      // Network error, retry
      attemptCount++
      if (attemptCount >= RETRY_DELAYS.length) {
        await recordFailure(webhook, event, error)
        return
      }
      await sleep(RETRY_DELAYS[attemptCount - 1])
    }
  }
}

Automatic Disabling

// Disable webhook after consecutive failures
const FAILURE_THRESHOLD = 10

if (webhook.consecutiveFailures >= FAILURE_THRESHOLD) {
  await db.update(webhooks)
    .set({ active: false })
    .where(eq(webhooks.id, webhook.id))
  
  // Notify owner
  await sendEmail({
    to: webhook.owner.email,
    subject: 'Webhook Disabled',
    body: `Webhook ${webhook.url} disabled after ${FAILURE_THRESHOLD} failures`
  })
}

๐ŸŽจ Frontend Implementation (Planned)

Webhook Management

export function WebhookManagement({ projectId }: Props) {
  const { data } = trpc.webhooks.list.useQuery({ projectId })
  const createWebhook = trpc.webhooks.create.useMutation()

  return (
    <div className="space-y-4">
      <div className="flex justify-between">
        <h2 className="text-xl font-bold">Webhooks</h2>
        <CreateWebhookDialog
          projectId={projectId}
          onSubmit={(values) => createWebhook.mutate(values)}
        />
      </div>

      <div className="space-y-2">
        {data?.webhooks.map((webhook) => (
          <Card key={webhook.id}>
            <CardHeader>
              <div className="flex items-center justify-between">
                <CardTitle className="text-sm font-mono">
                  {webhook.url}
                </CardTitle>
                <Badge variant={webhook.active ? 'default' : 'secondary'}>
                  {webhook.active ? 'Active' : 'Disabled'}
                </Badge>
              </div>
            </CardHeader>
            <CardContent>
              <div className="flex flex-wrap gap-1 mb-2">
                {webhook.events.map((event) => (
                  <Badge key={event} variant="outline">
                    {event}
                  </Badge>
                ))}
              </div>
              
              <div className="flex gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => testWebhook(webhook.id)}
                >
                  Test
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => viewDeliveries(webhook.id)}
                >
                  Deliveries
                </Button>
                <Button
                  variant="destructive"
                  size="sm"
                  onClick={() => deleteWebhook(webhook.id)}
                >
                  Delete
                </Button>
              </div>
            </CardContent>
          </Card>
        ))}
      </div>
    </div>
  )
}

Delivery History

export function DeliveryHistory({ webhookId }: Props) {
  const { data } = trpc.webhooks.listDeliveries.useQuery({ webhookId })

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Event</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Attempts</TableHead>
          <TableHead>Delivered At</TableHead>
          <TableHead>Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {data?.deliveries.map((delivery) => (
          <TableRow key={delivery.id}>
            <TableCell>{delivery.event}</TableCell>
            <TableCell>
              <StatusBadge status={delivery.responseStatus} />
            </TableCell>
            <TableCell>{delivery.attemptCount}</TableCell>
            <TableCell>
              {delivery.deliveredAt ? format(delivery.deliveredAt, 'PPp') : '-'}
            </TableCell>
            <TableCell>
              {delivery.failedAt && (
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => redeliver(delivery.id)}
                >
                  Retry
                </Button>
              )}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

๐Ÿงช Testing Webhooks

Local Development

# Use ngrok or similar to expose local server
ngrok http 3000

# Add webhook with ngrok URL
https://abc123.ngrok.io/webhooks/izri

Mock Webhook Server

import express from 'express'

const app = express()

app.post('/webhooks/izri', express.json(), (req, res) => {
  console.log('Received webhook:', req.body)
  console.log('Headers:', req.headers)
  
  // Verify signature
  const signature = req.headers['x-izri-signature']
  // ... verification logic
  
  res.json({ received: true })
})

app.listen(3000)

๐Ÿ”— Related Documentation


Next: Notifications โ†’

Reading this with an agent? /docs/features/webhooks.md serves the raw markdown.

All docs