Supabase Edge Function webhook for Sume: JWT off, HMAC on

Sume's webhook POST carries no Supabase JWT, so deploy the Edge Function with verify_jwt = false and check Sume's HMAC signature on every delivery.

5 min readSume
All posts

A Supabase Edge Function can receive Sume video webhooks once Supabase's JWT check is off for it: Edge Functions require a valid JWT by default, and Sume's POST carries none. Set verify_jwt = false for that function in supabase/config.toml, and authenticate every delivery with Sume's HMAC signature instead, using verifyWebhook imported from npm:@sume-com/sdk.

Supabase facts come from its Function Configuration, Handling Stripe Webhooks, Background Tasks, Limits, and related pages under Sources; Sume facts come from Run webhooks, Verifying webhooks, and Runs and results. All were read on 2026-09-27. There is no Sume connector for Supabase: this is a Deno function that Sume POSTs to. The general webhook contract is in Signed webhooks for video runs.

Why does the Edge Function reject Sume's POST?

By default, Edge Functions require a valid JWT in the authorization header, and Sume sends no Supabase token. Supabase documents the fix for this case, with Stripe webhooks as its example: a per-function [functions.sume-webhook] entry in supabase/config.toml with verify_jwt = false. When serving locally, the --no-verify-jwt flag on supabase functions serve does the same.

Supabase warns that this lets anyone invoke the function without a valid JWT. So the Sume signature check is not optional here: it is what stands between the public URL and your database. Supabase's own Stripe webhook function follows the same pattern, wrapping its handler in withSupabase({ auth: 'none' }), where the starter template from supabase functions new expects a publishable or secret key.

What does the Edge Function look like?

Edge Functions import npm packages with npm: specifiers, and @sume-com/sdk runs on Deno. Read the body with await req.text() before any JSON.parse, as the Stripe example also does, because verification needs the raw bytes. verifyWebhook returns false rather than throwing.

// supabase/functions/sume-webhook/index.ts
import { withSupabase } from "npm:@supabase/server@^1";
import { verifyWebhook } from "npm:@sume-com/sdk@0.2.0";

// Sume signs every delivery, so deploy with verify_jwt = false.
export default {
  fetch: withSupabase({ auth: "none" }, async (req) => {
    const body = await req.text(); // raw, before JSON.parse
    const ok = await verifyWebhook({
      body,
      headers: req.headers,
      secret: Deno.env.get("SUME_COM_WEBHOOK_SIGNING_SECRET")!,
    });
    if (!ok) return new Response("bad signature", { status: 401 });

    const event = JSON.parse(body);
    if (event.event !== "format.run.terminal") return new Response(null, { status: 204 });
    const fresh = await recordOnce(event.request_id, event); // insert-or-ignore
    if (fresh) EdgeRuntime.waitUntil(notifyUser(event)); // runs after the 204
    return new Response(null, { status: 204 });
  }),
};

How long can the function keep working after it answers?

EdgeRuntime.waitUntil(promise) keeps the instance running until the promise completes, without blocking the response. It is still capped by the wall-clock, CPU, and memory limits, so it suits a notification or a row update, not waiting on more generation. In local tests, the CLI terminates instances after each request, which stops background tasks early; Supabase's fix is policy = "per_worker" under [edge_runtime] in supabase/config.toml.

From Supabase's Limits page and Sume's Run webhooks and Formats pages, read 2026-09-27.
LimitValue
Sume delivery attempt10 s per attempt, up to 10 attempts
Edge Function wall clock150 s on Free, 400 s on paid plans
CPU time2 s per request, not counting async I/O
Request idle timeout150 s; without a response by then, 504 Gateway Timeout
Memory256MB
Sume long-form host video runTypically 15 to 30 minutes

Where do the secret and the URL come from?

Set both up once, before the first run:

  • Copy the signing secret from the Webhooks tab of the Sume dashboard, or read it from GET /v1/webhooks/signing-secret with a key carrying account:read.
  • Store it with supabase secrets set SUME_COM_WEBHOOK_SIGNING_SECRET=…. Secrets are available immediately, with no redeploy, and a name cannot start with SUPABASE_. The function reads it with Deno.env.get.
  • Deploy with supabase functions deploy sume-webhook. The function then runs at https://[YOUR_PROJECT_ID].supabase.co/functions/v1/sume-webhook, a public HTTPS URL; pass it as communication.webhook_url when you start a run.
  • The local server at http://localhost:54321/functions/v1/… cannot receive deliveries, because Sume rejects localhost and non-HTTPS URLs. See Test Sume webhooks on localhost.

What should the function store?

Key the stored row on request_id, which repeats on every retry, and keep payload.primary_output_url with it. Format run media URLs on media.sume.com are durable, but public to anyone holding the URL, so proxy or copy them if your app needs per-user access control. A receipt over 1 MiB arrives with payload: null; fetch it from error.result_url with your API key, stored as another secret. Signed webhooks for video runs covers the rest of the delivery contract.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume