Supabase Integration
Receive and route Supabase webhooks for database changes, authentication events, and more.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Supabase Production",
"slug": "supabase",
"verificationConfig": {
"type": "hmac",
"secret": "your-supabase-webhook-secret",
"algorithm": "sha256",
"header": "x-supabase-signature",
"encoding": "hex"
}
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/supabase2. Configure Supabase Webhooks
Database Webhooks
Database webhooks fire on row-level changes (INSERT, UPDATE, DELETE) for specified tables.
- Go to Supabase Dashboard
- Select your project
- Navigate to Database → Webhooks
- Click Create a new hook
- Select the table and events (Insert, Update, Delete)
- Set the Type to HTTP Request
- Enter your Hookbase URL as the URL
- Under HTTP Headers, add:
- Header:
x-supabase-signature - Value: Use a generated HMAC signature (see Signature Verification)
- Header:
- Click Create webhook
Auth Webhooks via Edge Functions
For authentication events (sign-up, sign-in, password reset), create an Edge Function that forwards to Hookbase:
// supabase/functions/auth-webhook/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts"
import { createHmac } from "node:crypto"
serve(async (req) => {
const payload = await req.json()
const body = JSON.stringify(payload)
const secret = Deno.env.get("WEBHOOK_SECRET")!
const signature = createHmac("sha256", secret)
.update(body)
.digest("hex")
await fetch("https://api.hookbase.app/ingest/{orgSlug}/supabase", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-supabase-signature": signature
},
body
})
return new Response(JSON.stringify({ success: true }), {
headers: { "Content-Type": "application/json" }
})
})Then configure the Auth Hook in Authentication → Hooks to call your Edge Function.
3. Create Destinations and Routes
# Create a destination for your backend
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "App Backend", "url": "https://api.myapp.com/webhooks/supabase"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "Supabase to Backend", "sourceId": "src_...", "destinationIds": ["dst_..."]}'Signature Verification
Supabase database webhooks can be configured to include an HMAC-SHA256 signature for verification. The signature is sent in the x-supabase-signature header as a hex-encoded string.
Hookbase automatically verifies this when configured:
{
"verificationConfig": {
"type": "hmac",
"secret": "your-supabase-webhook-secret",
"algorithm": "sha256",
"header": "x-supabase-signature",
"encoding": "hex"
}
}TIP
Use a strong, randomly generated secret (at least 32 characters). You must use the same secret both in your Supabase webhook configuration and in the Hookbase source.
Generating a Webhook Secret
# Generate a secure random secret
openssl rand -hex 32Use this secret in both your Supabase webhook configuration and the Hookbase source verificationConfig.
Common Events
Database — INSERT
Triggered when a new row is inserted into a watched table.
{
"type": "INSERT",
"table": "orders",
"schema": "public",
"record": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"status": "pending",
"total": 149.99,
"currency": "usd",
"items": [
{ "product_id": "prod_001", "quantity": 2, "price": 49.99 },
{ "product_id": "prod_002", "quantity": 1, "price": 50.01 }
],
"created_at": "2026-03-07T14:22:31.000Z"
},
"old_record": null
}Database — UPDATE
Triggered when a row is updated. Includes both the new and previous values.
{
"type": "UPDATE",
"table": "orders",
"schema": "public",
"record": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"status": "shipped",
"total": 149.99,
"currency": "usd",
"tracking_number": "1Z999AA10123456784",
"updated_at": "2026-03-07T16:45:12.000Z"
},
"old_record": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"status": "pending",
"total": 149.99,
"currency": "usd",
"tracking_number": null,
"updated_at": "2026-03-07T14:22:31.000Z"
}
}Database — DELETE
Triggered when a row is deleted. The old_record contains the deleted data.
{
"type": "DELETE",
"table": "sessions",
"schema": "public",
"record": null,
"old_record": {
"id": "sess_9f8e7d6c-b5a4-3210-fedc-ba9876543210",
"user_id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"expires_at": "2026-03-08T14:22:31.000Z",
"created_at": "2026-03-07T14:22:31.000Z"
}
}Auth — User Sign-Up
Triggered when a new user registers.
{
"type": "auth.user.created",
"table": "",
"schema": "",
"record": {
"id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"email": "[email protected]",
"phone": "",
"role": "authenticated",
"email_confirmed_at": "2026-03-07T14:22:31.000Z",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"full_name": "Jane Smith",
"avatar_url": ""
},
"created_at": "2026-03-07T14:22:31.000Z",
"updated_at": "2026-03-07T14:22:31.000Z"
},
"old_record": null
}Auth — User Sign-In
Triggered when a user signs in.
{
"type": "auth.user.signed_in",
"table": "",
"schema": "",
"record": {
"id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"email": "[email protected]",
"phone": "",
"role": "authenticated",
"last_sign_in_at": "2026-03-07T18:05:44.000Z",
"app_metadata": {
"provider": "email",
"providers": ["email", "google"]
},
"user_metadata": {
"full_name": "Jane Smith",
"avatar_url": "https://lh3.googleusercontent.com/a/example"
},
"created_at": "2026-02-15T09:12:00.000Z",
"updated_at": "2026-03-07T18:05:44.000Z"
},
"old_record": null
}Auth — Password Reset
Triggered when a user requests a password reset.
{
"type": "auth.user.updated",
"table": "",
"schema": "",
"record": {
"id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"email": "[email protected]",
"role": "authenticated",
"recovery_sent_at": "2026-03-07T19:30:00.000Z",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"full_name": "Jane Smith"
},
"updated_at": "2026-03-07T19:30:00.000Z"
},
"old_record": {
"id": "usr_8f3a2b1c-d4e5-6789-0abc-def123456789",
"email": "[email protected]",
"role": "authenticated",
"recovery_sent_at": null,
"updated_at": "2026-03-06T12:00:00.000Z"
}
}Supabase Event Types
| Category | Events |
|---|---|
| Database | INSERT, UPDATE, DELETE on any specified table |
| Auth | auth.user.created, auth.user.signed_in, auth.user.updated, auth.user.deleted |
Transform Examples
Slack Alert for New Database Records
function transform(payload) {
const record = payload.record || {};
const table = payload.table;
const type = payload.type;
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: `${type} on ${table}`
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Table:*\n\`${payload.schema}.${table}\``
},
{
type: "mrkdwn",
text: `*Operation:*\n${type}`
}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Record:*\n\`\`\`${JSON.stringify(record, null, 2).substring(0, 2000)}\`\`\``
}
}
]
};
}Database Format
function transform(payload) {
return {
event_type: payload.type,
table_name: `${payload.schema}.${payload.table}`,
record_id: payload.record?.id || payload.old_record?.id || null,
new_data: payload.record,
old_data: payload.old_record,
changed_fields: getChangedFields(payload.record, payload.old_record),
received_at: new Date().toISOString()
};
}
function getChangedFields(newRecord, oldRecord) {
if (!newRecord || !oldRecord) return null;
const changed = {};
for (const key of Object.keys(newRecord)) {
if (JSON.stringify(newRecord[key]) !== JSON.stringify(oldRecord[key])) {
changed[key] = { from: oldRecord[key], to: newRecord[key] };
}
}
return Object.keys(changed).length > 0 ? changed : null;
}Slack Alert for Auth Events
function transform(payload) {
const user = payload.record;
const eventType = payload.type;
const labels = {
"auth.user.created": "New User Sign-Up",
"auth.user.signed_in": "User Sign-In",
"auth.user.updated": "User Updated",
"auth.user.deleted": "User Deleted"
};
return {
channel: "#auth-events",
username: "Supabase Bot",
icon_emoji: ":key:",
attachments: [{
color: eventType === "auth.user.created" ? "good" : "#439FE0",
title: labels[eventType] || eventType,
fields: [
{ title: "Email", value: user.email || "N/A", short: true },
{ title: "Provider", value: user.app_metadata?.provider || "N/A", short: true },
{ title: "User ID", value: user.id, short: false }
],
footer: `Supabase Auth | ${eventType}`,
ts: Math.floor(Date.now() / 1000)
}]
};
}Filter Examples
INSERT Events Only
{
"name": "Inserts Only",
"logic": "and",
"conditions": [
{
"field": "type",
"operator": "equals",
"value": "INSERT"
}
]
}Specific Table
{
"name": "Orders Table Only",
"logic": "and",
"conditions": [
{
"field": "table",
"operator": "equals",
"value": "orders"
}
]
}Auth Events Only
{
"name": "Auth Events",
"logic": "and",
"conditions": [
{
"field": "type",
"operator": "starts_with",
"value": "auth."
}
]
}New User Sign-Ups
{
"name": "New Users Only",
"logic": "and",
"conditions": [
{
"field": "type",
"operator": "equals",
"value": "auth.user.created"
}
]
}Status Changes on Orders
{
"name": "Order Status Changes",
"logic": "and",
"conditions": [
{
"field": "type",
"operator": "equals",
"value": "UPDATE"
},
{
"field": "table",
"operator": "equals",
"value": "orders"
}
]
}Headers
Supabase sends these headers with database webhooks:
| Header | Description |
|---|---|
x-supabase-signature | HMAC-SHA256 signature (hex-encoded) for payload verification |
Content-Type | Always application/json |
User-Agent | Supabase webhook user agent |
TIP
When using Edge Functions to forward auth events, you control which headers are sent. Make sure to include the x-supabase-signature header with a valid HMAC signature.
Testing
Using Supabase SQL Editor
Trigger database webhooks by running SQL in the Supabase SQL Editor:
-- Trigger an INSERT webhook
INSERT INTO orders (id, user_id, status, total, currency)
VALUES (gen_random_uuid(), 'usr_test', 'pending', 29.99, 'usd');
-- Trigger an UPDATE webhook
UPDATE orders SET status = 'shipped' WHERE id = 'your-order-id';
-- Trigger a DELETE webhook
DELETE FROM sessions WHERE expires_at < now();Using curl
Send a test payload directly to your Hookbase source:
SECRET="your-supabase-webhook-secret"
PAYLOAD='{"type":"INSERT","table":"orders","schema":"public","record":{"id":"test-123","status":"pending","total":29.99},"old_record":null}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://api.hookbase.app/ingest/{orgSlug}/supabase \
-H "Content-Type: application/json" \
-H "x-supabase-signature: $SIGNATURE" \
-d "$PAYLOAD"Best Practices
Separate sources by environment: Use different Hookbase sources for staging and production Supabase projects
Be selective with tables: Only enable webhooks on tables that need external notification — avoid high-traffic tables like analytics or logs
Use filters for routing: Route INSERT/UPDATE/DELETE events to different destinations using filters
Handle idempotently: Database triggers can fire multiple times during retries — use
record.idfor deduplicationMonitor old_record: For UPDATE events, compare
recordandold_recordto detect meaningful changesSecure your secrets: Rotate webhook secrets periodically and store them securely
Troubleshooting
Signature Verification Failed
- Verify the
secretin your Hookbase source matches the secret used to generate the signature in Supabase - Ensure the signature header name matches — it must be
x-supabase-signature - Check that the encoding is set to
hex(notbase64) - If using an Edge Function, verify the HMAC is computed over the raw JSON body string
Webhooks Not Firing
- In Supabase Dashboard, go to Database → Webhooks and check the hook is enabled
- Verify the correct table and events (INSERT, UPDATE, DELETE) are selected
- Make sure the table has had actual changes — webhooks only fire on real data modifications
- Check Supabase logs in Logs → Postgres for trigger errors
Missing old_record on UPDATE/DELETE
- Supabase must have
REPLICA IDENTITYset on the table forold_recordto be populated - Run:
ALTER TABLE your_table REPLICA IDENTITY FULL;in the SQL Editor - Without this,
old_recordmay benullor contain only primary key columns
Auth Webhooks Not Arriving
- Auth webhooks require an Edge Function or external endpoint configured in Authentication → Hooks
- Verify the Edge Function is deployed and running (
supabase functions list) - Check Edge Function logs in Logs → Edge Functions for errors
- Ensure the Edge Function has the
WEBHOOK_SECRETenvironment variable set
Duplicate Events
Enable deduplication using the record ID:
curl -X PATCH .../sources/src_supabase \
-d '{"dedupEnabled": true, "dedupStrategy": "payload_field", "dedupField": "record.id"}'