Skip to content

Supabase Integration

Receive and route Supabase webhooks for database changes, authentication events, and more.

Setup

1. Create a Source in Hookbase

bash
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}/supabase

2. Configure Supabase Webhooks

Database Webhooks

Database webhooks fire on row-level changes (INSERT, UPDATE, DELETE) for specified tables.

  1. Go to Supabase Dashboard
  2. Select your project
  3. Navigate to DatabaseWebhooks
  4. Click Create a new hook
  5. Select the table and events (Insert, Update, Delete)
  6. Set the Type to HTTP Request
  7. Enter your Hookbase URL as the URL
  8. Under HTTP Headers, add:
  9. 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:

typescript
// 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 AuthenticationHooks to call your Edge Function.

3. Create Destinations and Routes

bash
# 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:

json
{
  "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

bash
# Generate a secure random secret
openssl rand -hex 32

Use 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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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

CategoryEvents
DatabaseINSERT, UPDATE, DELETE on any specified table
Authauth.user.created, auth.user.signed_in, auth.user.updated, auth.user.deleted

Transform Examples

Slack Alert for New Database Records

javascript
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

javascript
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

javascript
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

json
{
  "name": "Inserts Only",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "equals",
      "value": "INSERT"
    }
  ]
}

Specific Table

json
{
  "name": "Orders Table Only",
  "logic": "and",
  "conditions": [
    {
      "field": "table",
      "operator": "equals",
      "value": "orders"
    }
  ]
}

Auth Events Only

json
{
  "name": "Auth Events",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "starts_with",
      "value": "auth."
    }
  ]
}

New User Sign-Ups

json
{
  "name": "New Users Only",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "equals",
      "value": "auth.user.created"
    }
  ]
}

Status Changes on Orders

json
{
  "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:

HeaderDescription
x-supabase-signatureHMAC-SHA256 signature (hex-encoded) for payload verification
Content-TypeAlways application/json
User-AgentSupabase 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:

sql
-- 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:

bash
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

  1. Separate sources by environment: Use different Hookbase sources for staging and production Supabase projects

  2. Be selective with tables: Only enable webhooks on tables that need external notification — avoid high-traffic tables like analytics or logs

  3. Use filters for routing: Route INSERT/UPDATE/DELETE events to different destinations using filters

  4. Handle idempotently: Database triggers can fire multiple times during retries — use record.id for deduplication

  5. Monitor old_record: For UPDATE events, compare record and old_record to detect meaningful changes

  6. Secure your secrets: Rotate webhook secrets periodically and store them securely

Troubleshooting

Signature Verification Failed

  1. Verify the secret in your Hookbase source matches the secret used to generate the signature in Supabase
  2. Ensure the signature header name matches — it must be x-supabase-signature
  3. Check that the encoding is set to hex (not base64)
  4. If using an Edge Function, verify the HMAC is computed over the raw JSON body string

Webhooks Not Firing

  1. In Supabase Dashboard, go to DatabaseWebhooks and check the hook is enabled
  2. Verify the correct table and events (INSERT, UPDATE, DELETE) are selected
  3. Make sure the table has had actual changes — webhooks only fire on real data modifications
  4. Check Supabase logs in LogsPostgres for trigger errors

Missing old_record on UPDATE/DELETE

  1. Supabase must have REPLICA IDENTITY set on the table for old_record to be populated
  2. Run: ALTER TABLE your_table REPLICA IDENTITY FULL; in the SQL Editor
  3. Without this, old_record may be null or contain only primary key columns

Auth Webhooks Not Arriving

  1. Auth webhooks require an Edge Function or external endpoint configured in AuthenticationHooks
  2. Verify the Edge Function is deployed and running (supabase functions list)
  3. Check Edge Function logs in LogsEdge Functions for errors
  4. Ensure the Edge Function has the WEBHOOK_SECRET environment variable set

Duplicate Events

Enable deduplication using the record ID:

bash
curl -X PATCH .../sources/src_supabase \
  -d '{"dedupEnabled": true, "dedupStrategy": "payload_field", "dedupField": "record.id"}'

Released under the MIT License.