Supabase Integration
Receive and route Supabase Database Webhooks for row-level changes (INSERT, UPDATE, DELETE).
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",
"provider": "supabase"
}'Supabase does not issue a secret for Database Webhooks, so Hookbase generates one. Leave
signingSecret out and the create response returns the generated value once, in source.signingSecret
— copy it now. (You can also pass your own value as signingSecret, e.g. from
openssl rand -hex 32.)
Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/supabaseInfo
Supabase does not sign Database Webhooks. There is no Supabase signature header. Instead, you add
a custom Authorization HTTP header to the webhook in Supabase, set to the source's secret, and
Supabase sends it verbatim on every trigger. Hookbase's supabase provider checks that header against
the stored secret.
2. 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 Integrations → 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:
Authorization - Value: the Hookbase source's secret from step 1 (with or without
Bearerin front — both are accepted)
- Header:
- Click Create webhook
Auth Events
Database Webhooks only emit INSERT, UPDATE and DELETE. For sign-up, sign-in and token events,
Supabase has a separate product, Auth Hooks (Authentication → Hooks), which is signed
(Standard Webhooks, with a secret Supabase generates in the form v1,whsec_...). Receive those on a
separate Hookbase source with "provider": "supabase-auth-hooks" and paste the hook's secret as its
signingSecret. Don't reuse the supabase source: a Database Webhooks source would not verify an Auth
Hook, and vice versa.
Auth Hooks are synchronous — Supabase uses the hook's HTTP response to continue sign-in or token issuance — so check that hook's response contract in Supabase's Auth Hooks docs before pointing it at Hookbase.
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", "slug": "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_...", "destinationId": "dst_..."}'Signature Verification
Supabase Database Webhooks carry no signature. What Hookbase checks is the custom Authorization
header you added in step 2: with "provider": "supabase", the header value must equal the source's
secret exactly. Nothing is hashed, so the header must arrive byte-for-byte as stored.
{
"provider": "supabase",
"signingSecret": "the-value-in-your-authorization-header"
}Tip
Use a strong, random secret (at least 32 characters) — the one Hookbase generates qualifies. The
same value goes in the Supabase webhook's Authorization header and on the Hookbase source. Because
the header is a plain shared secret rather than a signature, anyone who sees it can reuse it: rotate it
on both sides if it leaks.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have requests without the right header refused with 401 instead of stored.
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"
}
}Supabase Event Types
| Category | Events |
|---|---|
| Database | INSERT, UPDATE, DELETE on any specified table |
| Auth | Not sent by Database Webhooks — use Auth Hooks with the supabase-auth-hooks provider |
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;
}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"
}
]
}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 |
|---|---|
Authorization | The custom header you added in Supabase, checked against the source's secret |
Content-Type | Always application/json |
User-Agent | Supabase webhook user agent |
Hookbase does not forward these to your destination. A delivery is a fresh request with
Hookbase's own X-Delivery-ID and X-Event-ID plus the headers and auth you configured on the
destination — nothing Supabase sent reaches your handler as a header. See
what headers a delivery carries.
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-hookbase-source-secret"
PAYLOAD='{"type":"INSERT","table":"orders","schema":"public","record":{"id":"test-123","status":"pending","total":29.99},"old_record":null}'
curl -X POST https://api.hookbase.app/ingest/{orgSlug}/supabase \
-H "Content-Type: application/json" \
-H "Authorization: $SECRET" \
-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 deduplication -
Monitor old_record: For UPDATE events, compare
recordandold_recordto detect meaningful changes -
Secure your secrets: Rotate the
Authorizationvalue periodically — update it in Supabase and on the Hookbase source together
Troubleshooting
Signature Verification Failed
- Confirm the source's
providerissupabase. A source left oncustomlooks for an HMAC signature that Supabase never sends - Confirm the webhook's HTTP header is named
Authorizationand its value equals the source's secret exactly — noBearerprefix, no surrounding spaces or quotes - If you rotated the source's secret in Hookbase, update the header value in Supabase too (Integrations → Database Webhooks → your hook)
- For Auth Hooks, use a separate source with
"provider": "supabase-auth-hooks"— see Auth Events
Webhooks Not Firing
- In Supabase Dashboard, go to Integrations → 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 Events Not Arriving
- Database Webhooks never send auth events. Configure an HTTPS hook in Authentication → Hooks instead
- Point it at a separate Hookbase source with
"provider": "supabase-auth-hooks", and paste the hook'sv1,whsec_...secret as that source'ssigningSecret
Duplicate Events
Supabase sends no event id header, so deduplication with the
auto strategy keys on a hash of the payload — an identical retried payload is dropped, while two
different changes to the same row are both kept:
curl -X PATCH .../sources/11111111-1111-4111-8111-111111110025 \
-d '{"dedupEnabled": true, "dedupStrategy": "auto"}'