Skip to content

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).

  • 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 uses phone and whatsapp_opt_in.
  1. Copy the function folder supabase/functions/notify-whatsapp from the example into your project, and add this block to your supabase/config.toml:

    supabase/config.toml
    [functions.notify-whatsapp]
    verify_jwt = false

    The 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).

  2. Create the secret the database will send, then store it and your key as function secrets:

    openssl rand -hex 32
    supabase secrets set TN_API_KEY=YOUR_KEY WEBHOOK_SECRET=THE_VALUE_FROM_ABOVE
  3. Deploy (the CLI bundles the function in a container, so Docker has to be running):

    supabase functions deploy notify-whatsapp
  4. 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, method POST.
    • HTTP header: x-webhook-secret with the value you generated.

    Creating it in the dashboard keeps the secret out of your repository, and the dashboard turns on the pg_net extension that webhooks use. The example also has the same webhook as SQL in supabase/webhook.sql, with placeholders. On a hosted project, enable Database Webhooks once in the dashboard before applying it: the SQL uses the supabase_functions schema, and a project that never enabled webhooks does not have it, so the file fails with schema "supabase_functions" does not exist and changes nothing. Its first statement turns on pg_net; without that extension, every insert and update on the table fails.

  5. Change what is sent in message.ts: it decides which events send a message and what the text says.

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.

Change the function and run supabase functions deploy notify-whatsapp again. The Turbo Notify address never changes.

  • 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 502 when trying again can help (a rate limit, the number is not connected, a Turbo Notify outage) and 200 with 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 answers 500 and 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_net directly. We have not run supabase/webhook.sql itself 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.