Send Standard Webhooks from Supabase using Database Triggers

- Name
- Luciano Bianchi
Supabase Database Triggers are a great way to start sending webhooks from Supabase. They keep the webhooks we send to our users consistent with what's in our database, without spinning up a new service or writing complex code. This article shows how to call Svix, the webhooks service we build, from a database function, so every database event sends a signed Standard Webhooks message to your users.
Supabase and Standard Webhooks
Standard Webhooks is an open specification for sending webhooks securely and consistently. We co-created it, and Supabase's Auth lead sits on its technical steering committee alongside people from Zapier, Twilio, Kong, and ngrok.
The spec defines three headers, webhook-id, webhook-timestamp, and webhook-signature, and how to sign them. The signature is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with a secret that only the sender and the receiver know. HMAC is what makes this work: nobody without the secret can produce a valid signature, so a receiver that recomputes it knows the request really came from you and wasn't changed on the way. Signing the timestamp too lets receivers reject an old request that an attacker captured and is replaying.
Supabase sends webhooks from two places, and only one of them follows the spec:
| Auth Hooks (HTTP) | Database Webhooks | |
|---|---|---|
| Fired by | Supabase Auth, during signup, login, and token issuance | A Postgres trigger on INSERT, UPDATE, or DELETE |
| Signature | Standard Webhooks, HMAC-SHA256 | None |
| Message ID | webhook-id header | None |
| Retries | Up to three on a 429 or 503 with a retry-after header, within 5 seconds | None |
Verifying a Supabase Auth Hook
If you're here because an Auth Hook won't verify, the culprit is usually the secret. Supabase issues it as v1,whsec_<base64>, and the Standard Webhooks libraries only strip a leading whsec_, so strip the whole prefix yourself. Then verify against the raw body, before parsing any JSON:
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
const wh = new Webhook(Deno.env.get('SEND_EMAIL_HOOK_SECRET')!.replace('v1,whsec_', ''))
Deno.serve(async (req) => {
const payload = await req.text()
const event = wh.verify(payload, Object.fromEntries(req.headers)) // throws if the signature is invalid
// ...send the email using event.user and event.email_data
return new Response('{}', { headers: { 'Content-Type': 'application/json' } })
})
Deploy the Edge Function with --no-verify-jwt, since Supabase Auth calls hooks without a user JWT and the signature is the authentication. And reply with Content-Type: application/json even on errors, or Supabase Auth turns your response into a 500.
Why Database Webhooks aren't enough for your users
Database Webhooks are a thin wrapper around a trigger and the pg_net extension. That's great for calling your own Edge Function, but it's not something to point at your customers.
There's no signature, so your users can't tell your webhook from a forged one. There's no retry, so a deploy that takes their endpoint down for thirty seconds silently loses those events. And there's no per-customer endpoint management or delivery log, so every "did you send it?" support ticket ends with someone querying net._http_response, which only keeps responses for six hours.
That's the gap Svix fills. Svix signs every message under the Standard Webhooks scheme, retries failed deliveries with exponential backoff, and gives each of your users a portal to add endpoints and inspect deliveries. Its svix-id, svix-timestamp, and svix-signature headers are aliases of the spec's webhook-* headers with identical values, so your users can verify them with the Svix libraries or any Standard Webhooks library.
Set up Svix with Supabase database triggers
Create a Svix account
Sign up with Svix and follow the Getting Started instructions. Get your API Authentication Token from the Svix dashboard.
Save the auth token in Supabase Vault
Create a secret in Supabase Vault to save the Svix API Key. We'll use it to call the Svix API from functions in Supabase.
SELECT vault.create_secret('<your-svix-api-key>', 'SVIX_API_KEY', 'Svix API Key');
Create a Svix application
In Svix, applications are where messages are sent to. In most cases, you would want to have one application for each of your users. In this example, we'll manually create an application with a curl command.
curl -X 'POST' \
'https://api.eu.svix.com/api/v1/app/' \
-H 'Authorization: Bearer <SVIX_API_KEY>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "My application",
"uid": "unique-app-identifier"
}'
In a real setting, the Svix application can be created automatically through the API when the user signs up. We could use a similar database function like the one we use to send messages.
Create a function to send a message to Svix
In Supabase, declare the function that will call Svix to send a webhook. It will be in charge of building the webhook payload and deciding the application id where the message will be sent to.
We're using an example invoices table that contains information for each new invoice we create, along with the user_id that the invoice belongs to.
CREATE OR REPLACE FUNCTION public.invoice_created_webhook()
RETURNS trigger
SET search_path = public
SECURITY DEFINER
LANGUAGE 'plpgsql'
AS
$$
DECLARE
svix_api_key text;
payload jsonb;
svix_response jsonb;
BEGIN
-- Read SVIX API Key from the vault
SELECT decrypted_secret INTO svix_api_key FROM vault.decrypted_secrets WHERE name = 'SVIX_API_KEY' LIMIT 1;
-- Generate the webhook payload
payload = jsonb_build_object(
'eventType', 'invoice.created',
'payload', new
);
-- Send the SVIX request
SELECT
net.http_post(
'https://api.svix.com/api/v1/app/' || new.user_id || '/msg/', -- using user_id as the Svix app id. This can change depending on your setup.
payload,
'{}',
jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || svix_api_key
),
'1000'
) INTO svix_response;
RETURN new;
END;
$$;
Create a database trigger
To send a webhook every time a database event occurs, create a trigger that will fire the webhook function declared above.
CREATE OR REPLACE TRIGGER invoice_created_webhook
AFTER INSERT
ON public.invoices
FOR EACH ROW
EXECUTE FUNCTION invoice_created_webhook();
In this example, every time a new row is added to the table, a message will be sent.
Add webhook endpoints
We showed how to send messages, though they are not being sent to any specific URL. In order for users to start listening to our webhooks, we need to add endpoints.
Check out the Svix docs for reference on how to add webhook endpoints using the API or using the Svix App Portal.
That's it! Every new invoice now reaches your users as a signed Standard Webhooks message, with retries and delivery logs handled by Svix.
If your users run on Supabase too, receiving webhooks with Supabase Edge Functions covers the other side: verifying these signatures in an Edge Function.
If you want to know more about how to get started with Svix, check out our docs.
Ready to send webhooks?
Svix is the enterprise ready webhooks service. Build a secure, reliable, and scalable webhook platform in minutes.