Appearance
Outbound Webhooks — Technical Reference
Architecture Overview
Outbound webhooks enable users to POST selected row data to external HTTP endpoints. The feature extends the existing bulk action pipeline with a new WEBHOOK_SEND operation.
Message Flow
Frontend (spread app)
↓ POST /api/data/bulk/{viewUuid}/webhook-send
BulkActionResource (Quarkus REST)
↓
BulkActionService (validates permissions, looks up WebhookConfig)
↓
WebhookDeliveryProducer (sends to webhook-delivery-queue)
↓
RabbitMQ Exchange "webhook-delivery" → Queue "webhook-delivery-queue"
↓
WebhookDeliveryConsumer (Spring Boot)
↓
BulkWebhookSendService (fetch rows → serialize → HMAC → HTTP POST)
↓
TaskCompletionProducer (sends BULK_WEBHOOK_SEND completion)
↓
BulkActionHandler (persists WebhookDelivery, broadcasts SSE)
↓
Frontend (receives completion, updates delivery log)Separate Queue
Webhook deliveries use a dedicated webhook-delivery-queue rather than the existing bulk-actions-queue. This ensures webhook sends don't compete with schema migrations or other bulk operations.
Database Schema
webhook_config
| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | autoIncrement |
| uuid | UUID UNIQUE | |
| view_id | BIGINT FK → view(id) CASCADE | |
| name | VARCHAR(100) | Display name |
| url | VARCHAR(2000) | Endpoint URL |
| headers_json | TEXT | JSON object of custom headers |
| hmac_secret | VARCHAR(256) | Optional HMAC-SHA256 secret |
| enabled | BOOLEAN DEFAULT true | |
| created_at / updated_at | TIMESTAMPTZ |
webhook_delivery
| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | autoIncrement |
| uuid | UUID UNIQUE | |
| webhook_config_id | BIGINT FK → webhook_config(id) CASCADE | |
| job_id | UUID | References BulkActionJob |
| attempt_number | INT DEFAULT 1 | |
| status_code | INT | HTTP response status |
| response_body | TEXT | Truncated to 4KB |
| error_message | VARCHAR(500) | |
| duration_ms | INT | Round-trip time |
| delivered_at | TIMESTAMPTZ |
REST API
Webhook Config CRUD
GET /api/data/webhooks/{viewUuid}— List configsPOST /api/data/webhooks/{viewUuid}— Create configPUT /api/data/webhooks/{viewUuid}/{webhookUuid}— Update configDELETE /api/data/webhooks/{viewUuid}/{webhookUuid}— Delete configPATCH /api/data/webhooks/{viewUuid}/{webhookUuid}/toggle?enabled=true— ToggleGET /api/data/webhooks/{viewUuid}/{webhookUuid}/deliveries— Delivery log
Webhook Send
POST /api/data/bulk/{viewUuid}/webhook-send— Initiate bulk webhook send
Security
SSRF Protection
WebhookConfigService.validateUrl() blocks:
- Private IP ranges (10.x, 172.16-31.x, 192.168.x)
- Loopback addresses (127.x, localhost)
- Link-local addresses (169.254.x)
- Internal hostnames (*.local, *.internal)
HMAC Signing
When an HMAC secret is configured, the payload is signed with HMAC-SHA256:
X-SchemaStack-Signature: sha256=<hex-encoded-hmac>Permissions
- Webhook config CRUD requires
CONFIGURE_VIEWpermission (ADMIN role) - Sending to webhooks requires
BULK_WEBHOOK_SENDpermission (EDITOR+ with view.exportable)
Key Files
| File | Module | Purpose |
|---|---|---|
| WebhookConfig.java | metadata-persistence | Entity |
| WebhookDelivery.java | metadata-persistence | Entity |
| WebhookConfigRepository.java | metadata-persistence | Repository |
| WebhookDeliveryRepository.java | metadata-persistence | Repository |
| WebhookConfigService.java | metadata-service | CRUD + SSRF + HMAC |
| WebhookConfigResource.java | metadata-rest | REST endpoints |
| WebhookDeliveryProducer.java | metadata-producer | RabbitMQ producer |
| BulkWebhookSendService.java | processor-core | Row fetch + HTTP delivery |
| WebhookDeliveryConsumer.java | processor-consumer | RabbitMQ consumer |
| BulkActionHandler.java | metadata-consumer | Completion handler + SSE |