Supabase Database Webhooks are one of those features that look trivial until you try to develop against them. A row lands in orders, a Postgres trigger fires through pg_net, and an HTTP request goes out to some URL. Great — except that URL has to be publicly reachable. Your handler runs on localhost:3000, and there is no way to point a hosted Postgres instance at a port on your laptop.
So you end up in the usual loop: deploy a half-finished handler to staging, insert a test row, squint at logs, tweak, redeploy. Every iteration costs minutes. And because Supabase Database Webhooks don't HMAC-sign their payloads, you can't even confidently test your verification logic without shipping it first.
Relayers fixes the reachability problem. You get a public ingest URL, point the database webhook at it, and forward every delivery straight to your local server over a tunnel — original body and headers untouched.
What you'll build
Prerequisites
- A Supabase project with a table you want to watch (we'll use
orders). - A local handler listening on a port — say
localhost:3000. - A free Relayers account.
- The
relayersCLI or the desktop app installed.
1Create an endpoint in Relayers
Open the desktop app, go to Endpoints, and create a new one. You'll get a public ingest URL that looks like this:
https://api.relayers.app/v1/webhooks/wep_1a2b3c4d5e
Copy it. Hit Send test if you want to confirm the endpoint is live before wiring up Supabase. This URL is the single stable target — everything downstream (filtering, transforming, tunneling) is configured in Relayers, so you never touch the Supabase side again once it's set.
2Create the Database Webhook in Supabase
In the Supabase dashboard, go to Database → Webhooks → Create a new hook:
- Name — something like
orders_to_localhost. - Table — select
ordersin thepublicschema. - Events — check the operations you care about. For this walkthrough,
INSERT,UPDATE, andDELETE. - Type — choose HTTP Request.
- Method —
POST. - URL — paste your Relayers ingest URL from step 1.
- HTTP Headers — add a custom auth header. Since Supabase Database Webhooks don't sign their payloads, this shared secret is how you'll verify authenticity later:
X-Webhook-Secret: super-secret-value-you-generate
Save the hook. From now on, every matching row change fires an HTTP POST to Relayers with a payload shaped like this:
{
"type": "INSERT",
"table": "orders",
"record": { "id": 42, "amount": 1999, "status": "paid" },
"old_record": null,
"schema": "public"
}
3Forward events to localhost
Log in and start the tunnel from your terminal:
relayers login
relayers listen --forward localhost:3000
That's it. Insert a row in Supabase (or use the SQL editor: insert into orders (amount, status) values (1999, 'paid');) and the delivery lands on localhost:3000 within seconds. The desktop app's Events view shows each delivery, and the payload inspector lets you expand the exact JSON body and headers Supabase sent — including your X-Webhook-Secret.
If a delivery fails while your server is down, you can retry it from the Events view once you're back up.
4Route and filter with JQ
You rarely want every event hitting the same handler. Open Rules on your endpoint and add a JQ filter so only new orders get forwarded:
{ "type": "INSERT", "table": "orders" }
Rules are evaluated top to bottom, first match wins, and each rule has a destination — a tunnel to localhost:PORT or a public URL. You can reorder them by dragging. So you might route INSERT events to your local dev server and DELETE events to a separate audit URL, all from the same Supabase webhook.
5Transform the payload
The raw Supabase envelope is noisy for most handlers. In Transformations, reshape it to exactly what your endpoint expects. Using jq:
{ event: .type, table: .table, id: .record.id }
Given the earlier payload, your handler now receives:
{ "event": "INSERT", "table": "orders", "id": 42 }
Relayers supports jq, JSONata, JavaScript, and Go templates, and there's a test playground so you can paste a sample payload and see the output before you save. No redeploys to iterate on your mapping.
Verifying authenticity (no built-in signature)
This is the part worth calling out. Unlike Stripe or GitHub, Supabase Database Webhooks do not HMAC-sign their requests. There is no signature header to verify against a signing secret.
The practical answer is the custom header you added in step 2. Relayers forwards the original headers untouched, so your local handler sees X-Webhook-Secret exactly as Supabase sent it. Verify it with a constant-time comparison:
import crypto from "node:crypto";
function verify(req) {
const got = req.headers["x-webhook-secret"] ?? "";
const want = process.env.WEBHOOK_SECRET ?? "";
const a = Buffer.from(got);
const b = Buffer.from(want);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Because the header survives the tunnel intact, the verification code you test locally is the exact same code that runs in production.
Going to production
When you're ready to deploy, keep the Relayers endpoint and just change the destination on your rule from the tunnel to your public URL (for example https://api.yourapp.com/webhooks/supabase). The Supabase webhook config never changes — it still points at the same wep_<id> ingest URL. Filtering, transformation, and header pass-through all behave identically.
If you manage multiple environments, use Import/Export to move an endpoint's rules and transformations as a JSON bundle between projects.
Troubleshooting
- Nothing arrives. Supabase webhooks run through
pg_net, which is asynchronous — the HTTP request is queued, not sent inside your transaction. There's a small delay, and failures don't roll back your insert. Give it a few seconds, then check the Relayers Events view. If the event isn't there at all, the request never left Supabase. - Wrong events firing. Double-check the table and the INSERT/UPDATE/DELETE checkboxes in the hook config. A common mistake is enabling only
UPDATEand testing with anINSERT. - Handler rejects the request. If your auth check fails, confirm the
X-Webhook-Secretheader is set in the Supabase hook and matchesWEBHOOK_SECRETlocally. Use the payload inspector to see the exact header Relayers received. - Filter never matches. Test your JQ against a real captured payload from the Events view. Remember the operation is in
.type(uppercaseINSERT/UPDATE/DELETE), and the row is under.record, not the top level.
Wrap-up
Supabase Database Webhooks are a clean way to turn Postgres row changes into HTTP calls, but the missing pieces — local reachability and signature verification — make them awkward to develop against. Relayers gives you a stable public ingest URL, a tunnel to localhost, JQ filtering, payload transformation, and untouched header pass-through so your verification logic works the same locally and in production.
Create a free endpoint at relayers.app and download the app to start forwarding your Supabase database webhooks to localhost in a couple of minutes.