Turbo Notify in Supabase
When a row changes in your Supabase project, your customer gets a WhatsApp message. A Database Webhook calls an Edge Function, and the function sends the message through the Turbo Notify API. The function is small, has no dependencies and comes with its tests, in the examples repository (opens in a new tab).
What you need
Section titled “What you need”- A Turbo Notify account with a connected number (quickstart).
- An API key with only the permission to send messages (
messages:send), created in API Keys (opens in a new tab). - A Supabase project and the Supabase CLI, signed in and linked to the project.
- A table with the customer’s phone number in international format (
+and the country code, no spaces) and a column that records whether the person agreed to receive WhatsApp messages. The example usesphoneandwhatsapp_opt_in.
Connect
Section titled “Connect”-
Copy the function folder
supabase/functions/notify-whatsappfrom the example into your project, and add this block to yoursupabase/config.toml:supabase/config.toml [functions.notify-whatsapp]verify_jwt = falseThe database calls the function without a user token, so the platform’s token check is off for this function. The function checks a secret of its own instead (next step).
-
Create the secret the database will send, then store it and your key as function secrets:
openssl rand -hex 32supabase secrets set TN_API_KEY=YOUR_KEY WEBHOOK_SECRET=THE_VALUE_FROM_ABOVE -
Deploy (the CLI bundles the function in a container, so Docker has to be running):
supabase functions deploy notify-whatsapp -
In the Supabase dashboard, open Integrations, then Database Webhooks, and create a webhook:
- Table: yours (the example uses
orders), with the Insert and Update events. - Type: Supabase Edge Functions, function
notify-whatsapp, methodPOST. - HTTP header:
x-webhook-secretwith the value you generated.
Creating it in the dashboard keeps the secret out of your repository, and the dashboard turns on the
pg_netextension that webhooks use. The example also has the same webhook as SQL insupabase/webhook.sql, with placeholders. On a hosted project, enable Database Webhooks once in the dashboard before applying it: the SQL uses thesupabase_functionsschema, and a project that never enabled webhooks does not have it, so the file fails withschema "supabase_functions" does not existand changes nothing. Its first statement turns onpg_net; without that extension, every insert and update on the table fails. - Table: yours (the example uses
-
Change what is sent in
message.ts: it decides which events send a message and what the text says.
Sign in
Section titled “Sign in”Turbo Notify needs only the API key, and it stays a Supabase secret: never in a table, never in the repository. Create the key with just messages:send. If it leaks, the most it can do is send messages, and you can revoke it in API Keys (opens in a new tab) without touching anything else.
The function has a second guard on the Supabase side: it accepts a call only if it carries the x-webhook-secret you set, so nobody can trigger a message by calling the function address.
First, a test that sends nothing. Call the function with a row that did not agree to receive messages (replace the address and the secret):
curl -X POST https://YOUR_PROJECT.supabase.co/functions/v1/notify-whatsapp \ -H "x-webhook-secret: THE_VALUE_FROM_ABOVE" \ -d '{"type":"INSERT","table":"orders","schema":"public","record":{"id":1,"phone":"+12025550123","whatsapp_opt_in":false},"old_record":null}'The answer is {"skipped":"no_opt_in"}. With a wrong secret the answer is 401.
Then the real one: insert a row in your table with your own number and the opt-in column true. A few seconds later the message arrives on your phone. The function answers the database right away; whether the message was delivered arrives later, by webhook or polling.
To try the whole path without a hosted project, run it on the local Supabase stack. The example’s README (opens in a new tab) has the commands. We ran exactly that path: a row with consent produced the message on the phone, the same event sent twice produced one message, and a row without consent sent nothing.
Update
Section titled “Update”Change the function and run supabase functions deploy notify-whatsapp again. The Turbo Notify address never changes.
Limits
Section titled “Limits”- Consent is yours. Send only to people who agreed to receive WhatsApp messages from you. The example refuses a row where the opt-in column is not true, so a missing record of consent stops the message instead of sending it.
- A number connected like WhatsApp Web. Turbo Notify connects your number as a linked device, with no Meta sign-up and no template approval. It is an independent service, not affiliated with Meta, and every send follows a pace that helps protect the number. WhatsApp decides on restrictions, and a number can be restricted whatever the pace. The practices that keep it healthy are in Protect your number.
- Your plan’s quota and pace apply to every message the function sends, as to any other send. See Rate limits.
- One event, one message. The function sends a key with each message that identifies the event, so the same event delivered twice is sent once.
- Failures. The function answers
502when trying again can help (a rate limit, the number is not connected, a Turbo Notify outage) and200with the reason when it cannot (an invalid request). When the account is the problem (the subscription is not active, or the plan’s message or contact allowance is used up until the cycle turns) or the key is wrong, it answers500and the reason appears in the function’s logs, so the notifications are not dropped quietly. - Hosted projects. We ran this path on the local Supabase stack and on a new hosted project: the table, the secrets and the deploy, a row with consent producing the message, the status change producing the second one, and the same event posted again giving back the same message. On the hosted project the database called the function through
pg_netdirectly. We have not runsupabase/webhook.sqlitself there, nor the webhook form in the dashboard. The function does not depend on Supabase calling it again after a failed answer: the same event is sent once either way.