What you will build
Prerequisites
- A Supabase project, and somewhere to host an HTTPS route
- A published to send
Secure it with a shared secret
Supabase Database Webhooks are built onpg_net, and they do not sign the payload. Clerk uses Svix and Stripe signs the raw bytes, so both let you prove a request came from them. Supabase gives you custom HTTP headers instead.
That means the check is yours to add, and it is not optional. Your route will sit on a public URL, and without a secret anyone who finds it can post a fake signup.
Generate a long random value, add it as a header on the webhook, and compare it in the handler with a constant-time comparison.
Set it up
1
Mirror new signups into a public table
Point the webhook at a table in
public, not at auth.users directly.The auth schema belongs to the supabase_auth_admin role, which holds only the permissions it needs for authentication. A trigger firing there and reaching outside the schema hits permission denied for schema auth. Most Supabase apps already keep a profiles table for exactly this reason.security definer is the load-bearing line. Without it the trigger runs as supabase_auth_admin and cannot write to public.2
Create the webhook
In the Supabase Dashboard, go to Database → Webhooks and create one:
- Table:
public.profiles - Events:
Insert - Type: HTTP Request,
POST, pointing at your route - HTTP Headers: add
x-webhook-secretwith your random value
record is the new row and old_record is the previous one, which is null on an insert.3
Write the route handler
Compare the secret before doing anything else, and use The trigger already flattened
timingSafeEqual rather than === so the comparison does not leak the value a character at a time.app/api/supabase/route.ts
raw_user_meta_data into a column, so the handler reads record.full_name rather than digging through the auth payload.4
Add the environment values
.env.local
Verify
1
Create a user
Sign up through your app, or add a user from Authentication → Users in the Supabase Dashboard.
2
Check the webhook ran
Supabase records each delivery with its response code under the webhook in Database → Webhooks. A
401 means the secret does not match.3
Confirm the send
Open and confirm the message.
Adapt it for your own tables
The payload shape is the same for every table inpublic. That makes the same handler useful well beyond signup.
The UPDATE row is the one worth planning for. Because
old_record carries the previous values, your handler decides whether the change is worth a notification. Firing on every update is how a useful alert becomes noise.