Multi-Tenant Webhook Architecture
This guide demonstrates how to implement a multi-tenant webhook system using Hook0's label-based routing mechanism. Organizations can manage webhooks for millions of users and projects with complete isolation and visibility into delivery attempts.
We use GitLab.com as a practical example to illustrate the concepts, but this architecture applies to any multi-tenant SaaS platform.
Multi-Tenant Architecture Challenge
A multi-tenant platform hosting millions of projects across thousands of organizations needs:
- Reliable webhook delivery with automatic retries
- Visibility into webhook logs and delivery attempts
- Multi-tenant isolation ensuring data privacy
- Scalable infrastructure handling billions of events
Set Up Environment Variables
# Set your service token (from dashboard)
export HOOK0_TOKEN="YOUR_TOKEN_HERE"
export HOOK0_API="https://app.hook0.com/api/v1" # Replace by your domain (or http://localhost:8081 locally)
# Set your application ID (shown in dashboard URL or application details)
export APP_ID="YOUR_APPLICATION_ID_HERE"
Save these values:
# Save to .env file for later use
cat > .env <<EOF
HOOK0_TOKEN=$HOOK0_TOKEN
HOOK0_API=$HOOK0_API
APP_ID=$APP_ID
EOF
Architecture Overview
Hook0 provides multi-tenant platforms with:
- Multi-tenant event routing using labels
- Request attempt tracking for user visibility
- Automatic retry logic with exponential backoff
- Secure isolation between tenants
Step 1: Create Event Types
First, define event types for your platform activities. In this GitLab example:
# Create push event type
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "gitlab",
"resource_type": "push",
"verb": "created"
}'
# Create merge request event type
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "gitlab",
"resource_type": "merge_request",
"verb": "opened"
}'
# Create pipeline event type
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "gitlab",
"resource_type": "pipeline",
"verb": "completed"
}'
Step 2: Multi-Tenant Event Ingestion
When events occur in your platform, send them to Hook0 with tenant-specific labels:
# Example: User pushes code to a project
# Note: event_id must be a valid UUID
curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'"$(uuidgen)"'",
"event_type": "gitlab.push.created",
"payload": "{\"ref\":\"refs/heads/main\",\"commits\":3,\"author\":\"john@example.com\",\"message\":\"Fix bug in authentication\"}",
"payload_content_type": "application/json",
"occurred_at": "2024-01-15T14:30:00Z",
"labels": {
"namespace_id": "12345",
"project_id": "67890",
"group_id": "54321",
"user_id": "usr_abc123",
"project_path": "acme-corp/api-gateway",
"visibility": "private",
"plan": "premium",
"ref": "main"
}
}'
# Example: Pipeline completion event
curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'"$(uuidgen)"'",
"event_type": "gitlab.pipeline.completed",
"payload": "{\"pipeline_id\":\"9876\",\"status\":\"success\",\"duration\":240,\"stages\":[\"test\",\"build\",\"deploy\"]}",
"payload_content_type": "application/json",
"occurred_at": "2024-01-15T14:45:00Z",
"labels": {
"namespace_id": "12345",
"project_id": "67890",
"pipeline_id": "9876",
"environment": "production",
"triggered_by": "usr_abc123"
}
}'
Step 3: User Webhook Management
Users create webhooks that are registered as Hook0 subscriptions with label-based filtering:
# User creates a project webhook via GitLab UI
# GitLab backend registers it with Hook0:
curl -X POST "$HOOK0_API/subscriptions" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["gitlab.push.created", "gitlab.merge_request.opened"],
"description": "ACME Corp API Gateway project webhook",
"labels": {
"project_id": "67890"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://ci.acme-corp.com/webhooks/gitlab",
"headers": {
"X-Custom-Header": "acme-api-gateway"
}
},
"metadata": {
"gitlab_webhook_id": "webhook_123",
"created_by_user": "usr_abc123",
"project_path": "acme-corp/api-gateway"
}
}'
# Group-level webhook (triggers for all projects in group)
curl -X POST "$HOOK0_API/subscriptions" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["gitlab.pipeline.completed"],
"description": "ACME Corp group-wide pipeline notifications",
"labels": {
"group_id": "54321"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://monitoring.acme-corp.com/pipelines",
"headers": {}
},
"metadata": {
"gitlab_group_webhook_id": "group_webhook_456",
"group_path": "acme-corp"
}
}'
Step 4: Exposing Logs and Request Attempts to Users
Expose webhook delivery information through your UI by querying Hook0's API:
# Get request attempts for a subscription (webhook)
# GitLab UI calls this when user views webhook logs
curl -X GET "$HOOK0_API/request_attempts/?application_id=$APP_ID&subscription_id={SUBSCRIPTION_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN"
# Response includes delivery attempts with status, response codes, and timing
# GitLab formats this data for display in their webhook settings UI
# Filter by time range for recent attempts
curl -X GET "$HOOK0_API/request_attempts/?application_id=$APP_ID&subscription_id={SUBSCRIPTION_ID}&min_created_at=2024-01-15T00:00:00Z" \
-H "Authorization: Bearer $HOOK0_TOKEN"
# Get response body for debugging failed deliveries
# First get the response_id from request_attempt, then fetch response details
curl -X GET "$HOOK0_API/responses/{RESPONSE_ID}?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
Step 5: User-Facing Webhook Management
Users can manage their webhooks through your UI, which internally calls Hook0:
When updating subscriptions, the PUT endpoint requires ALL fields (application_id, is_enabled, event_types, labels, target), not just the ones you want to change. First GET the current subscription, then send the complete object with your modifications.
# List all webhooks for an application (GitLab queries subscriptions)
curl -X GET "$HOOK0_API/subscriptions/?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
# Update webhook (e.g., change URL or events) - include ALL fields
curl -X PUT "$HOOK0_API/subscriptions/{SUBSCRIPTION_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["gitlab.push.created", "gitlab.merge_request.opened", "gitlab.pipeline.completed"],
"description": "ACME Corp API Gateway project webhook",
"labels": {
"project_id": "67890"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://new-ci.acme-corp.com/webhooks",
"headers": {"Content-Type": "application/json"}
}
}'
# Disable webhook temporarily (all fields required)
curl -X PUT "$HOOK0_API/subscriptions/{SUBSCRIPTION_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": false,
"event_types": ["gitlab.push.created", "gitlab.merge_request.opened"],
"description": "ACME Corp API Gateway project webhook",
"labels": {
"project_id": "67890"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://ci.acme-corp.com/webhooks/gitlab",
"headers": {"Content-Type": "application/json"}
}
}'
# Delete webhook
curl -X DELETE "$HOOK0_API/subscriptions/{SUBSCRIPTION_ID}?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
Step 6: Retry Failed Webhook Deliveries
Users can manually retry failed webhook deliveries through your UI:
# Replay a specific event (user clicks "Retry" in GitLab UI)
curl -X POST "$HOOK0_API/events/{EVENT_ID}/replay" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'"
}'
Step 7: System Hooks for Platform Administrators
Platform administrators can set up system-wide hooks:
# Create system hook for user creation events
curl -X POST "$HOOK0_API/subscriptions" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["gitlab.user.created", "gitlab.user.blocked", "gitlab.project.created"],
"description": "GitLab.com system hooks for compliance",
"labels": {
"hook_type": "system"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://compliance.gitlab.com/system-events",
"headers": {
"X-System-Hook": "true"
}
}
}'
Step 8: Analytics and Monitoring
Monitor webhook health across your platform:
# Query delivery metrics by time range
curl -X GET "$HOOK0_API/request_attempts/?application_id=$APP_ID&min_created_at=2024-01-15T00:00:00Z&max_created_at=2024-01-15T23:59:59Z" \
-H "Authorization: Bearer $HOOK0_TOKEN"
# Monitor webhook performance for a specific subscription
curl -X GET "$HOOK0_API/request_attempts/?application_id=$APP_ID&subscription_id={SUBSCRIPTION_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN"
# Filter by event to track specific webhook deliveries
curl -X GET "$HOOK0_API/request_attempts/?application_id=$APP_ID&event_id={EVENT_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN"
Benefits for End Users
- Visibility: Users see all webhook delivery attempts, response codes, and error messages directly in your UI
- Reliability: Automatic retries with exponential backoff ensure events are delivered
- Debugging: Access to request/response bodies helps users debug integration issues
- Multi-tenancy: Complete isolation between different organizations and projects
- Scalability: Hook0 handles the infrastructure complexity of managing millions of webhooks
Security Considerations
- Each tenant (namespace/project/organization) has isolated webhook data
- Biscuit tokens include facts limiting access to specific tenants
- Request logs are retained according to user's plan (e.g., 7 days for free, 30 days for premium)
- Sensitive headers are redacted in logs
- Webhook signatures prevent tampering and replay attacks
Summary
By using Hook0, multi-tenant platforms can:
- Offload webhook delivery infrastructure to a specialized service
- Provide users with detailed delivery logs and retry capabilities
- Maintain strict multi-tenant isolation
- Scale to handle billions of events across millions of projects
- Focus on core business functionality rather than webhook infrastructure
The label-based routing system (labels) enables flexible filtering without requiring complex query languages, making it perfect for hierarchical multi-tenant structures (users, groups, organizations, projects, etc.).