Skip to main content
You can fire an HTTP request from inside Postgres when a row in a table changes. The mechanism is a trigger function (supabase_functions.http_request()) plus pg_net, the async HTTP extension, both preloaded in every Powabase project. Reach for it when you want to “tell our internal API every time a user signs up,” “post to Slack on order creation,” or “invalidate a cache when this row updates.” This is NOT the same as the agentic webhooks at /api/webhooks. Those are the inbound side: external systems triggering workflows. Database webhooks are the outbound side: Postgres rows changing and Postgres calling out to some HTTP endpoint. Don’t confuse them. For the agentic surface, see Webhooks reference. For pg_net specifics, see Extensions.

How it works

The platform installs pg_net in the extensions schema and a helper trigger function supabase_functions.http_request() that wraps pg_net.http_post. You attach the function as a trigger to your table:
Arguments to http_request:
  1. URL: where to POST.
  2. Method: typically POST; PUT/PATCH/DELETE also work.
  3. Headers (JSONB): your auth, content-type, custom headers.
  4. Params (JSONB): query string params, if any.
  5. Timeout (ms): how long pg_net waits before giving up.
The request body is the NEW row for INSERT/UPDATE triggers, serialized as JSON. For DELETE triggers, it’s the OLD row. The function doesn’t let you customize the body; see below for that.

Customizing the body

The default body is the bare row. For richer payloads, write your own trigger function that wraps pg_net.http_post:
pg_net.http_post returns a request_id. The actual HTTP response (or error) lands later in the net._http_response table, asynchronously.

Async semantics

pg_net is fire-and-forget from the trigger’s perspective. The HTTP request runs in a background worker; the trigger function returns immediately after queuing it. Two implications:
  • The trigger doesn’t block on the HTTP response. Your INSERT commits as soon as the trigger queues the request. The HTTP call can fail without rolling back the INSERT.
  • You can’t get the response synchronously. If your trigger needs to know whether the call succeeded (for retry logic, for a transactional outbox pattern), you have to poll net._http_response.
For side-effect use cases like Slack notifications or cache invalidation, the async model is what you want. When the INSERT shouldn’t succeed unless the webhook went through, use a transactional outbox pattern instead: insert into an outbox table inside the same transaction, then have a separate worker (or workflow) drain the outbox and call the webhook.

Querying responses

To see how recent webhook calls went:
status_code is the HTTP response code (200, 4xx, 5xx). error_msg is populated on timeouts and connection errors. The net._http_response table accumulates rows over time and isn’t auto-pruned. For high-volume webhook senders, run a periodic cleanup:
Or set up a maintenance job via a workflow that runs nightly.

Retries

pg_net does not retry failed requests. If your endpoint is down when the trigger fires, the request is lost. Three retry patterns, depending on what you need: Option 1: Application-level retry on the receiving end. The webhook arrives once; if the receiver wants idempotency, it dedupes by an event id you include in the payload. Option 2: Outbox table + scheduled retry. Insert “I want to send X” rows into a public.webhook_outbox table inside the same transaction as the INSERT that triggers it. A scheduled workflow drains the outbox, calls the webhook, and marks rows as sent (or failed-retry). Option 3: Use the agentic /api/webhooks surface in the other direction. Have your trigger call a deployed workflow URL; the workflow handles retries and observability inside Powabase. This trades async-HTTP simplicity for workflow visibility. Most webhooks should be retryable on the receiver side (option 1). Reserve options 2/3 for cases where you need delivery guarantees.

Common patterns

Slack notification on row insert

The body is the new orders row serialized as JSON. Slack’s webhook format expects { "text": "..." }, so the raw row won’t format nicely. Use a custom function (see “Customizing the body” above) to build a Slack-shaped payload.

Cache invalidation on update

Audit log to S3

To ship every row change off-platform for auditing, point the webhook at an HTTPS endpoint that writes to S3: a Lambda, a small webhook service, or your own ingestion pipeline. Because pg_net is async, your INSERT doesn’t wait, and the audit writes happen on the side.

DB webhooks vs Realtime postgres_changes

Both fire on row changes. The difference:
DB webhookRealtime postgres_changes
TransportHTTP POSTWebSocket
ReceiverAny HTTP endpointConnected clients
PersistenceNone (pg_net queues, but no delivery guarantee)None (WS subscribers see live changes only)
RetriesNone built-inNone
Use caseBackend-to-backend integrationClient UI updates
If your receivers are clients (browsers, mobile apps), use Realtime. If they’re backend services / SaaS integrations, use DB webhooks.

Next steps

Webhooks reference

The inbound side: external systems triggering workflows via /api/webhooks. A different surface, often confused with this one.

Realtime model

The browser-friendly alternative for change notifications.

Extensions

pg_net and what else lives in the extensions schema.

Direct Postgres

For installing triggers via psql or migrations.