Database Webhooks for Self-Hosted Supabase: Complete Setup Guide

Learn how to configure database webhooks on self-hosted Supabase with pg_net, handle common pitfalls, and build event-driven workflows.

Cover Image for Database Webhooks for Self-Hosted Supabase: Complete Setup Guide

Database webhooks let your self-hosted Supabase instance notify external services whenever data changes. When a row is inserted, updated, or deleted, Supabase can automatically fire an HTTP request to your application, a third-party API, or an Edge Function. For event-driven architectures, this is essential—and self-hosted Supabase fully supports it.

But setting up webhooks on a self-hosted instance isn't as straightforward as clicking a button in the dashboard. Between Docker networking quirks, the pg_net extension, and debugging silent failures, there's a learning curve. This guide walks you through configuring database webhooks properly, avoiding common mistakes, and building reliable event-driven workflows.

How Database Webhooks Work in Supabase

Database webhooks in Supabase are built on top of PostgreSQL triggers and the pg_net extension. When you create a webhook, Supabase generates a trigger that fires on INSERT, UPDATE, or DELETE events. Instead of blocking your transaction to make a synchronous HTTP call, pg_net handles requests asynchronously in a background worker.

This asynchronous approach has real advantages:

  • Non-blocking: Your database operations complete immediately—network latency doesn't slow down inserts
  • Resilient: A failed webhook won't roll back your transaction
  • Scalable: The pg_net extension handles up to 200 requests per second by default

The trade-off is that webhooks aren't guaranteed to fire in real-time. According to community reports on GitHub, some users have observed delays of 1+ seconds between row insertion and webhook delivery. For most use cases this is acceptable, but if you need sub-100ms delivery, you'll want to consider alternatives like PostgreSQL's NOTIFY/LISTEN pattern.

Enabling pg_net on Self-Hosted Supabase

Before you can use database webhooks, the pg_net extension must be enabled. On self-hosted installations, this is typically included by default, but you should verify it's active.

Connect to your database and run:

SELECT * FROM pg_extension WHERE extname = 'pg_net';

If no rows return, enable it:

CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;

You can also enable it through Supabase Studio by navigating to Database → Extensions and searching for "pg_net."

For self-hosted instances using Supascale, pg_net is pre-configured and enabled automatically when you create a new project.

Creating Your First Database Webhook

You have two options for creating webhooks: through the Supabase Studio UI or via SQL. The UI is convenient for quick setup, but SQL gives you version-controlled, reproducible webhooks.

Using Supabase Studio

  1. Open your self-hosted Supabase Studio
  2. Navigate to Database → Webhooks
  3. Click Create a new hook
  4. Select the table and events (INSERT, UPDATE, DELETE)
  5. Enter your webhook URL
  6. Add any headers (such as authorization tokens)

Using SQL

For production deployments, defining webhooks in SQL migrations is the better approach:

CREATE OR REPLACE FUNCTION notify_order_created()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM net.http_post(
    url := 'https://your-api.example.com/webhooks/orders',
    headers := jsonb_build_object(
      'Content-Type', 'application/json',
      'Authorization', 'Bearer ' || current_setting('app.webhook_secret', true)
    ),
    body := jsonb_build_object(
      'event', 'order.created',
      'data', row_to_json(NEW)
    )
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_order_insert
  AFTER INSERT ON orders
  FOR EACH ROW
  EXECUTE FUNCTION notify_order_created();

This approach lets you store webhook configurations in your migration files, making them reproducible across development, staging, and production environments.

The Docker Networking Gotcha

Here's where many self-hosters stumble. When your Supabase database runs inside a Docker container (which it does in most self-hosted setups), localhost and 127.0.0.1 refer to the container itself—not your host machine.

If you're trying to send webhooks to a service running on your host:

This won't work:

http://localhost:3000/webhooks

Use this instead:

http://host.docker.internal:3000/webhooks

On Linux, host.docker.internal may not work by default. You might need to use your machine's actual IP address or add a network alias in your Docker Compose configuration:

services:
  db:
    extra_hosts:
      - "host.docker.internal:host-gateway"

For production, your webhook endpoints should be on a domain or IP accessible from within the Docker network. If you're running everything through a reverse proxy like Nginx or Traefik, your internal services can communicate via container names or internal network addresses.

Securing Your Webhooks

Unsecured webhooks are a vulnerability. Anyone who discovers your endpoint URL could send fake payloads. Implement proper authentication:

Header-Based Authentication

Add a secret header to your webhook configuration:

PERFORM net.http_post(
  url := 'https://api.example.com/webhooks',
  headers := jsonb_build_object(
    'X-Webhook-Secret', 'your-32-char-secret-here'
  ),
  body := payload
);

Your receiving service validates this header before processing:

app.post('/webhooks', (req, res) => {
  if (req.headers['x-webhook-secret'] !== process.env.WEBHOOK_SECRET) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // Process webhook...
});

Signed Payloads with pgsodium

For stronger security, use pgsodium (included in Supabase) to cryptographically sign your payloads:

SELECT pgsodium.crypto_auth(
  message := convert_to(payload::text, 'utf8'),
  key := decode(current_setting('app.webhook_signing_key'), 'hex')
);

Your receiving service verifies the signature before trusting the payload. This prevents tampering even if the webhook secret is compromised.

Debugging Silent Failures

The most frustrating webhook issue is when they fail silently—your data inserts successfully, but the webhook never fires. Here's a systematic debugging approach:

1. Verify the Background Worker

pg_net uses a background worker to process requests. Check if it's running:

SELECT * FROM pg_stat_activity 
WHERE backend_type = 'pg_net worker';

If this returns no rows, the worker has crashed. On pg_net 0.8+, restart it with:

SELECT net.worker_restart();

2. Check the Request Queue

Pending requests sit in net.http_request_queue:

SELECT * FROM net.http_request_queue 
ORDER BY id DESC 
LIMIT 10;

If requests are queued but never processed, the background worker is the problem.

3. Review Response History

Completed requests (successful or failed) are stored in net._http_response:

SELECT id, status_code, content, created 
FROM net._http_response 
WHERE created > NOW() - INTERVAL '1 hour'
ORDER BY created DESC;

Look for non-2xx status codes or error messages in the content column. Responses are kept for 6 hours before being purged.

4. Check the Webhook Logs

In Supabase Studio, navigate to Database → Webhooks and check the delivery attempts for your webhook. This shows timing, status codes, and any error messages.

Webhooks vs. Edge Functions: When to Use Each

Self-hosted Supabase supports both database webhooks and Edge Functions. They solve different problems:

Use Database Webhooks when:

  • You need to notify an external service about database changes
  • The processing happens outside your Supabase instance
  • You want non-blocking, fire-and-forget notifications
  • You're integrating with third-party APIs (Slack, Stripe, custom backends)

Use Edge Functions when:

  • The logic runs on your Supabase infrastructure
  • You need access to Supabase client libraries and environment
  • You're transforming data or calling multiple services
  • You want easier debugging through function logs

For many workflows, you'll combine both: a database webhook triggers an Edge Function, which then orchestrates a complex workflow.

Production Best Practices

Critical vs. Non-Critical Webhooks

Not all webhooks deserve the same treatment:

For critical operations (payments, authentication):

  • Consider synchronous approaches using the http extension
  • Implement retry logic in your receiving service
  • Monitor webhook success rates actively

For non-critical operations (notifications, analytics):

  • pg_net's async approach is ideal
  • A failed webhook shouldn't block your application
  • Accept that some events might be lost under extreme conditions

Avoid Trigger Recursion

Never create webhooks on pg_net's internal tables:

-- DON'T DO THIS - causes infinite loops
CREATE TRIGGER bad_idea
  AFTER INSERT ON net._http_response
  FOR EACH ROW
  EXECUTE FUNCTION some_function();

Handle High-Traffic Tables Carefully

Dropping or modifying triggers on high-traffic tables requires exclusive locks. On busy tables, this can cause timeouts:

-- This might time out on busy tables
DROP TRIGGER my_webhook ON high_traffic_table;

Schedule these changes during low-traffic periods or use shorter lock timeouts:

SET lock_timeout = '5s';
DROP TRIGGER my_webhook ON orders;

Store Webhook Configurations in Code

Manually configured webhooks don't survive database resets and are hard to share across team members. Define webhooks in your migration files:

# Generate a new migration
supabase migration new add_order_webhooks

# Add your webhook SQL to the generated file
# supabase/migrations/xxx_add_order_webhooks.sql

This keeps webhook configurations version-controlled and reproducible across all your environments.

Simplifying Webhook Management with Supascale

Managing webhooks across multiple self-hosted Supabase projects adds operational overhead. When you're running five projects across staging and production, manually configuring webhooks in each becomes tedious and error-prone.

Supascale simplifies this by providing a unified management interface for all your self-hosted Supabase instances. You can configure webhooks once and deploy consistently across projects. Combined with automated backups, you won't lose webhook configurations when restoring from backup.

For teams running multiple Supabase instances, this reduces the operational burden of maintaining consistent configurations across environments.

Conclusion

Database webhooks transform self-hosted Supabase from a database into an event-driven platform. By understanding pg_net's async nature, handling Docker networking correctly, and implementing proper security, you can build reliable webhook integrations that scale with your application.

The key points to remember:

  • pg_net handles webhooks asynchronously—they won't block your transactions
  • Use host.docker.internal or real IPs when targeting services on your host machine
  • Always authenticate your webhooks with secrets or signatures
  • Debug silent failures by checking the background worker and response tables
  • Store webhook configurations in SQL migrations for reproducibility

Ready to simplify your self-hosted Supabase management? Check out Supascale's pricing—a one-time purchase for unlimited projects with full API access for automation.


Further Reading