Skip to content

Webhooks

Webhooks allow you to receive real-time notifications when events occur in your Fluxbase database. Instead of continuously polling for changes, webhooks push data to your application instantly.

Fluxbase webhooks trigger HTTP requests to your specified endpoint whenever database events occur, such as:

  • INSERT: A new row is added to a table
  • UPDATE: An existing row is modified
  • DELETE: A row is removed from a table

Terminal window
npm install @nimbleflux/fluxbase-sdk
import { createClient } from "@nimbleflux/fluxbase-sdk";
const client = createClient('http://localhost:8080', 'your-service-role-key')
// Create a webhook
const webhook = await client.management.webhooks.create({
name: "User Events",
description: "Notify external system of user changes",
url: "https://example.com/webhooks/fluxbase",
secret: "your-webhook-secret",
enabled: true,
events: [
{
table: "users",
operations: ["INSERT", "UPDATE"],
},
],
max_retries: 3,
timeout_seconds: 30,
});
console.log("Webhook created:", webhook.id);
// List all webhooks
const webhooks = await client.management.webhooks.list();
// Get webhook details
const details = await client.management.webhooks.get(webhook.id);
// Update webhook
await client.management.webhooks.update(webhook.id, {
enabled: false,
events: [
{
table: "users",
operations: ["INSERT", "UPDATE", "DELETE"],
},
],
});
// Delete webhook
await client.management.webhooks.delete(webhook.id);

const webhook = await client.management.webhooks.create({
name: "Order Notifications",
description: "Send notifications when orders are created or updated",
url: "https://api.myapp.com/webhooks/orders",
secret: "secure-random-secret-string",
enabled: true,
events: [
{
table: "orders",
operations: ["INSERT", "UPDATE"],
},
{
table: "order_items",
operations: ["INSERT"],
},
],
max_retries: 3,
timeout_seconds: 30,
retry_backoff_seconds: 5,
});
console.log("Webhook ID:", webhook.id);
console.log("Webhook URL:", webhook.url);

Configuration Options:

Field Type Description
name string Descriptive name for the webhook
description string Optional details about the webhook’s purpose
url string The endpoint that will receive webhook events
secret string Optional webhook secret for verifying requests
enabled boolean Whether the webhook is active
events array List of event configurations
max_retries number Retry attempts for failed deliveries (default: 3)
timeout_seconds number Request timeout in seconds (default: 30)
retry_backoff_seconds number Seconds between retries (default: 5)

Event Configuration:

{
table: "table_name", // Table to monitor
operations: ["INSERT", "UPDATE", "DELETE"] // Events to trigger on
}
const webhooks = await client.management.webhooks.list();
webhooks.forEach((webhook) => {
console.log(`${webhook.name}: ${webhook.enabled ? "Enabled" : "Disabled"}`);
console.log(` URL: ${webhook.url}`);
console.log(` Events: ${webhook.events.length} configured`);
});
const webhook = await client.management.webhooks.get("webhook-id");
console.log("Name:", webhook.name);
console.log("Enabled:", webhook.enabled);
console.log("Events:", webhook.events);
console.log("Max Retries:", webhook.max_retries);
console.log("Created:", webhook.created_at);
// Enable/disable webhook
await client.management.webhooks.update(webhookId, {
enabled: false,
});
// Update events
await client.management.webhooks.update(webhookId, {
events: [
{
table: "users",
operations: ["INSERT", "UPDATE", "DELETE"],
},
],
});
// Update URL and secret
await client.management.webhooks.update(webhookId, {
url: "https://new-endpoint.com/webhooks",
secret: "new-secret-key",
});
await client.management.webhooks.delete(webhookId);
console.log("Webhook deleted");
const deliveries = await client.management.webhooks.listDeliveries(webhookId, 50);
deliveries.forEach((delivery) => {
console.log(`${delivery.created_at}: ${delivery.status}`);
console.log(` Response: ${delivery.response_status}`);
console.log(` Attempts: ${delivery.attempts}`);
if (delivery.error) {
console.log(` Error: ${delivery.error}`);
}
});

Fluxbase signs all webhook requests with a timestamp-based HMAC-SHA256 signature to ensure authenticity and prevent replay attacks.

When a webhook has a secret configured, Fluxbase sends two signature headers:

Header Format Description
X-Fluxbase-Signature t=timestamp,v1=signature Timestamped signature (recommended)
X-Webhook-Signature hexstring Legacy signature (for backwards compatibility)

Example Header:

X-Fluxbase-Signature: t=1705678901,v1=5d41402abc4b2a76b9719d911017c592

The signature is computed over timestamp.payload:

signed_data = timestamp + "." + raw_request_body
signature = HMAC-SHA256(signed_data, webhook_secret)

The timestamp is included to prevent replay attacks - signatures older than 5 minutes should be rejected.

The TypeScript SDK does not include a built-in signature verification method. You should verify webhook signatures manually using HMAC-SHA256. See the Manual Verification (Node.js) example below.

import (
"github.com/nimbleflux/fluxbase/internal/webhook"
"time"
)
func handleWebhook(body []byte, signatureHeader, secret string) error {
// Verify with 5 minute tolerance
err := webhook.VerifyWebhookSignature(
body,
signatureHeader,
secret,
5*time.Minute,
)
if err != nil {
return fmt.Errorf("invalid signature: %w", err)
}
// Process webhook...
return nil
}
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, header, secret, toleranceMs = 300000) {
// rawBody must be the EXACT raw request body string (bytes) — re-serializing
// JSON (e.g. JSON.stringify) changes whitespace/key order and breaks the HMAC.
// Parse header: t=timestamp,v1=signature
const parts = header.split(',').reduce((acc, part) => {
const [key, value] = part.split('=');
if (key === 't') acc.timestamp = parseInt(value, 10);
if (key === 'v1') acc.signatures = [...(acc.signatures || []), value];
return acc;
}, {});
// Check timestamp (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parts.timestamp) > toleranceMs / 1000) {
throw new Error('Signature timestamp too old or in future');
}
// Compute expected signature over the raw body
const signedPayload = `${parts.timestamp}.${rawBody}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures (constant-time)
const isValid = parts.signatures.some(sig =>
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))
);
if (!isValid) {
throw new Error('Signature mismatch');
}
return true;
}
import hmac
import hashlib
import time
import json
def verify_webhook_signature(payload: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
# Parse header: t=timestamp,v1=signature
parts = {}
signatures = []
for part in header.split(','):
key, value = part.split('=', 1)
if key == 't':
parts['timestamp'] = int(value)
elif key == 'v1':
signatures.append(value)
# Check timestamp
now = int(time.time())
if abs(now - parts['timestamp']) > tolerance_seconds:
raise ValueError('Signature timestamp too old or in future')
# Compute expected signature
signed_payload = f"{parts['timestamp']}.{payload.decode('utf-8')}"
expected_sig = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures (constant-time)
if not any(hmac.compare_digest(sig, expected_sig) for sig in signatures):
raise ValueError('Signature mismatch')
return True

When an event occurs, Fluxbase sends a POST request to your webhook URL with the following payload:

{
"event": "INSERT",
"table": "users",
"schema": "public",
"record": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"created_at": "2025-11-02T10:30:00Z"
},
"old_record": null,
"timestamp": "2025-11-02T10:30:00.123Z"
}
Field Type Description
event string The operation type: “INSERT”, “UPDATE”, “DELETE”
table string Name of the table where the event occurred
schema string Database schema (usually “public”)
record object The new/current state of the record
old_record object|null Previous state (only for UPDATE and DELETE)
timestamp string ISO 8601 timestamp when the event occurred
{
"event": "INSERT",
"record": {
/* new record data */
},
"old_record": null
}
{
"event": "UPDATE",
"record": {
/* updated record data */
},
"old_record": {
/* previous record data */
}
}
{
"event": "DELETE",
"record": {
/* deleted record data */
},
"old_record": {
/* same as record */
}
}

const express = require("express");
const crypto = require("crypto");
const app = express();
app.post("/webhooks/fluxbase", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-fluxbase-signature"];
const secret = process.env.FLUXBASE_WEBHOOK_SECRET;
const isValid = verifyWebhookSignature(req.body, signature, secret);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body);
const { event, table, record, old_record } = payload;
switch (event) {
case "INSERT":
console.log("New record:", record.id);
break;
case "UPDATE":
console.log("Updated:", record.id);
break;
case "DELETE":
console.log("Deleted:", record.id);
break;
}
res.status(200).send("OK");
});
function verifyWebhookSignature(rawBody, header, secret, toleranceMs = 300000) {
const parts = header.split(",").reduce((acc, part) => {
const [key, value] = part.split("=");
if (key === "t") acc.timestamp = parseInt(value, 10);
if (key === "v1") acc.signatures = [...(acc.signatures || []), value];
return acc;
}, {});
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parts.timestamp) > toleranceMs / 1000) {
return false;
}
const signedPayload = `${parts.timestamp}.${rawBody}`;
const expectedSig = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
return parts.signatures.some((sig) =>
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))
);
}
app.listen(3000);

Fluxbase includes built-in protection against Server-Side Request Forgery (SSRF) attacks for webhooks.

SSRF allows attackers to trick your server into making requests to internal resources (databases, admin panels, cloud metadata services).

Fluxbase automatically blocks webhook requests to:

Type Blocked Resources
Private IP ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8
Cloud metadata 169.254.169.254 (AWS), metadata.google.internal
Internal services localhost, kubernetes.default.svc
Private hostnames Subdomains of internal services

SSRF protection is enabled by default:

webhook:
# ⚠️ WARNING: Only disable in development/testing
allow_private_ips: false # Default: false (SSRF protection enabled)
// ❌ BLOCKED: Private IP
await client.management.webhooks.create({
url: 'http://192.168.1.1/webhook',
// Error: "URL resolves to private IP address"
});
// ❌ BLOCKED: Cloud metadata
await client.management.webhooks.create({
url: 'http://169.254.169.254/latest/meta-data/',
// Error: "URL resolves to private IP address"
});
// ❌ BLOCKED: Localhost
await client.management.webhooks.create({
url: 'http://localhost:3000/webhook',
// Error: "localhost URLs are not allowed"
});
// ✅ ALLOWED: Public URLs
await client.management.webhooks.create({
url: 'https://api.example.com/webhook',
// Success!
});

For local development with internal services:

# Development config (NOT for production)
webhook:
allow_private_ips: true

Never enable allow_private_ips: true in production.

For more details, see SSRF Protection Guide.


Practice Description
Respond quickly Return 200 status immediately, process asynchronously if needed (30s timeout)
Handle duplicates Use record IDs to ensure idempotent processing
Verify signatures Always verify webhook signatures using HMAC-SHA256
Use HTTPS Secure webhook URLs with HTTPS in production
Log deliveries Keep detailed logs of events for debugging
Monitor failures Use client.management.webhooks.listDeliveries() to track failed deliveries

Example: Async processing

app.post("/webhooks", async (req, res) => {
res.status(200).send("OK"); // Respond immediately
processWebhookEvent(req.body).catch(console.error); // Process async
});

  • Send notifications: Trigger emails/push notifications on events
  • Sync data: Keep external systems (Salesforce, CRM) in sync
  • Analytics: Stream events to analytics platforms
  • Trigger workflows: Start automated workflows based on database changes

// Test via SDK
await client.management.webhooks.test(webhookId);
// Or use dashboard: Webhooks → Select webhook → Test

Monitor deliveries:

const deliveries = await client.management.webhooks.listDeliveries(webhookId, 100);

Retry behavior: Failed deliveries automatically retry (default: 3 attempts, exponential backoff, 30s timeout)

Common issues:

Issue Solution
Webhook not triggering Verify table name, operations configured, webhook enabled
Delivery failures Check endpoint is public, responds within timeout, valid SSL
Signature failing Use correct secret, verify with HMAC-SHA256 manually, use raw body