Run an Edge Function per new row: webhook in, secret checked

supabase functions new, deploy, and a webhook at /functions/v1/<name> with an Authorization header. Verify a shared secret, key the work on record.id.

Edge Functions & Webhooks

· Chapter

40

·

4

min read

The answer. Edge Functions are small Deno handlers deployed next to your project and invoked at /functions/v1/<name>. They are the right place for work that should not run in a Postgres transaction and should not need a separate server: enrich a new row, call a third-party API, generate an embedding. Our upload-asset function exists for exactly that reason: it streams up to 10 MB of image bytes to a CMS, work we did not want inside the API Worker's CPU budget. Wired to a Database Webhook, the shape is: the insert commits, pg_net POSTs the record to the function, the function verifies a shared secret, does the work keyed on the row id so a duplicate delivery is harmless, and writes the result back with a secret-key client.

The pattern.

npx supabase functions new enrich-ticket && npx supabase secrets set HOOK_SECRET=… OPENAI_API_KEY=…
npx supabase functions serve enrich-ticket        # local: http://localhost:54321/functions/v1/enrich-ticket
npx supabase functions deploy enrich-ticket
// supabase/functions/enrich-ticket/index.ts
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
  if (req.headers.get("x-hook-secret") !== Deno.env.get("HOOK_SECRET")) return new Response("forbidden", { status: 403 });
  const { type, record } = await req.json();
  if (type !== "INSERT") return new Response("ignored", { status: 204 });
  const db = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SECRET_KEY")!);   // set as a secret
  const { data: done } = await db.from("tickets").select("summary").eq("id", record.id).single();
  if (done?.summary) return new Response("already", { status: 200 });                                 // idempotent
  const summary = await summarize(record.title, record.description);
  await db.from("tickets").update({ summary }).eq("id", record.id);
  return new Response("ok");
});
create trigger tickets_enrich after insert on public.tickets for each row execute function supabase_functions.http_request(
  'https://<ref>.supabase.co/functions/v1/enrich-ticket', 'POST',
  '{"Content-Type":"application/json","Authorization":"Bearer <publishable key>","x-hook-secret":"…"}', '{}', '10000');

Watch out.

  • The gateway rejects calls without an Authorization: Bearer <key> header, even from a webhook or a cron job; the publishable key satisfies it, your own secret header does the real check. Set verify_jwt = false for the function only when you verify something else.
  • Function secrets (supabase secrets set) are separate from Vault and from your Worker secrets; three stores, three rotations.
  • Functions are for seconds of work with modest memory; image processing and long batches belong in a queue consumer.

Related: database-webhooks-are-pg-net-triggers-make-receivers-idempotent · pg-cron-inside-postgres-vs-a-worker-cron-outside · signed-urls-and-image-transforms-for-private-files