Supabase Edge Functions + reelsdk

Caption and render video from a Supabase Edge Function

Supabase Edge Functions run in Deno isolates with 256 MB of memory, 2 s of CPU per request and no subprocesses, so ffmpeg can't run there. reelsdk does the encode on its own machines: your function sends one fetch and gets an mp4 with word-by-word captions back, for $0.02 per output minute with the first $1 free.

  • Waiting on fetch doesn't count toward the 2 s CPU limit
  • Captions on a 30 s clip come back in 30 to 43 s
  • Signed webhook lands the mp4 in Supabase Storage
What your Edge Function gets today

Spawning subprocesses is not allowed on Supabase Edge Runtime.

546 Resource Limit: WORKER_RESOURCE_LIMIT

Source: Supabase Edge Functions status codes

The same job as one call to reelsdk

const REELSDK = "https://api.reelsdk.com";

const res = await fetch(`${REELSDK}/v1/captions?wait=120`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${Deno.env.get("REELSDK_KEY")}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ video_url, style: "karaoke" }),
});
const { job } = await res.json();
200 with job.outputs.video when it finishes inside the wait, 202 and a webhook when it doesn't

Why ffmpeg breaks here

256 MB, 2 s of CPU and no child processes

Edge Functions give each request 256 MB of memory and 2 s of CPU time, with a 20 MB bundle limit when you deploy from the CLI. Spawning a native ffmpeg binary throws Spawning subprocesses is not allowed on Supabase Edge Runtime., and anything that pushes memory or CPU past the cap during an encode stops the function with HTTP 546 and WORKER_RESOURCE_LIMIT.

ffmpeg.wasm is the usual next try. Since version 0.12 it only runs in the browser, so on the server it is out, and in the browser it downloads a core of about 31 MB and encodes on the visitor's device, which stalls phones and dies when the tab closes. Waiting on a network response is different: async I/O doesn't count toward the 2 s CPU budget, so a function can hold a fetch to reelsdk open for minutes.

Edge Function limits next to reelsdk

Edge Function limits next to reelsdk
LimitSupabase Edge Functionsreelsdk
Memory256 MBThe encode runs on reelsdk machines, inputs up to 500 MB each
CPU time2 s per request, async I/O excludedUp to 10 min of run time per job
Wall clock150 s on Free, 400 s on paid plans?wait= holds up to 300 s, or a webhook calls you back
SubprocessesNot allowedRaw ffmpeg 8.1 arguments through POST /v1/ffmpeg
Bundle size20 MB from the CLI, 5 MB from the dashboardNothing to bundle, one fetch

Supabase Edge Functions limits from Supabase Edge Functions limits, checked October 2026.

Set it up

Two Edge Functions: one starts the render, one receives it

You need a reelsdk key, its webhook secret and two small functions. The first starts a captions job and returns early if it runs long. The second receives the signed webhook and copies the mp4 into a Storage bucket.

  1. Store the key and the webhook secret

    Create a key in the reelsdk dashboard to get an rk_live_ API key and its whsec_ webhook secret. Save both as project secrets with the CLI and read them with Deno.env.get. The webhook function has to accept calls without a Supabase JWT, so deploy it with --no-verify-jwt or set verify_jwt = false under [functions.reelsdk-webhook] in supabase/config.toml.

    terminal
    supabase secrets set REELSDK_KEY=rk_live_xxx REELSDK_WEBHOOK_SECRET=whsec_xxx
    
    supabase functions deploy render-captions
    supabase functions deploy reelsdk-webhook --no-verify-jwt
  2. Start the captions job

    The function takes a video URL, for example a signed URL from Storage, and asks reelsdk to burn karaoke captions into it. ?wait=120 keeps the request open for up to two minutes, which covers most clips and stays inside the 150 s limit. If the job needs longer you get a 202 with the job id, and the webhook delivers the result.

    supabase/functions/render-captions/index.ts
    import "jsr:@supabase/functions-js/edge-runtime.d.ts";
    
    const REELSDK = "https://api.reelsdk.com";
    
    Deno.serve(async (req) => {
      const { video_url } = await req.json();
    
      const res = await fetch(`${REELSDK}/v1/captions?wait=120`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${Deno.env.get("REELSDK_KEY")}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          video_url,
          style: "karaoke",
          webhook_url: `${Deno.env.get("SUPABASE_URL")}/functions/v1/reelsdk-webhook`,
        }),
      });
    
      const { job, error } = await res.json();
      if (!res.ok) return Response.json({ error }, { status: res.status });
    
      // On 200 the mp4 is ready; on 202 we let the webhook deliver it
      return Response.json({ id: job.id, status: job.status, video: job.outputs?.video ?? null });
    });
  3. Verify the webhook and save the mp4

    reelsdk signs every delivery with Reel-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of t.body keyed with your whsec_ secret. Read the body as text before parsing, check the signature with Web Crypto, reject anything older than 300 s, then copy the file into Storage with EdgeRuntime.waitUntil so the function answers right away. reelsdk keeps its own link for 7 days.

    supabase/functions/reelsdk-webhook/index.ts
    import "jsr:@supabase/functions-js/edge-runtime.d.ts";
    import { createClient } from "npm:@supabase/supabase-js@2";
    
    const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
    
    Deno.serve(async (req) => {
      const body = await req.text();
      const signature = req.headers.get("Reel-Signature");
      if (!(await verifyReelSignature(signature, body, Deno.env.get("REELSDK_WEBHOOK_SECRET")!))) {
        return new Response("invalid signature", { status: 401 });
      }
    
      const job = JSON.parse(body);
      if (job.status === "succeeded") {
        // We answer right away and copy the mp4 into Storage in the background
        EdgeRuntime.waitUntil(saveToStorage(job.id, job.outputs.video));
      }
      return new Response("ok");
    });
    
    async function saveToStorage(id: string, url: string) {
      const video = await fetch(url);
      const { error } = await supabase.storage
        .from("videos")
        .upload(`${id}.mp4`, await video.blob(), { contentType: "video/mp4", upsert: true });
      if (error) console.error("storage upload failed", id, error.message);
    }
    
    async function verifyReelSignature(header: string | null, body: string, secret: string) {
      const parts = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=")));
      const age = Math.abs(Date.now() / 1000 - Number(parts.t));
      if (!/^[0-9a-f]{64}$/i.test(parts.v1 ?? "") || !(age <= 300)) return false;
    
      const encoder = new TextEncoder();
      const key = await crypto.subtle.importKey(
        "raw",
        encoder.encode(secret),
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["verify"],
      );
      const signature = Uint8Array.from(parts.v1.match(/../g), (pair: string) => parseInt(pair, 16));
      return crypto.subtle.verify("HMAC", key, signature, encoder.encode(`${parts.t}.${body}`));
    }

FAQ

Video on Supabase Edge Functions, answered

Can I run ffmpeg or ffmpeg.wasm inside a Supabase Edge Function?

No. The native binary fails with "Spawning subprocesses is not allowed on Supabase Edge Runtime." ffmpeg.wasm only runs in the browser since version 0.12, and any real encode would blow through the 2 s CPU limit and end with HTTP 546 WORKER_RESOURCE_LIMIT. Send the job to an API like reelsdk and wait on the response instead.

Will waiting for a render time out my Edge Function?

Waiting on fetch is async I/O, so it doesn't count toward the 2 s CPU limit. The wall clock limit is 150 s on the Free plan and 400 s on paid plans. Pass ?wait=120 to hold the connection for most jobs, or ?wait=0 to get a 202 right away and let the webhook deliver the mp4.

How long does a render take?

A render machine is ready in 1.87 s at the median. Burning word-by-word captions into a 30 s clip takes 30 to 43 s, a 30 s 1080x1920 slideshow with voiceover and captions takes 40 to 45 s, and a faceless short takes about 100 s. Each job can run for up to 10 minutes on media up to 15 minutes long.

How do I secure the webhook function if verify_jwt is off?

Verify the request yourself. Read the Reel-Signature header, check that the timestamp t is no more than 300 s old, and compute an HMAC-SHA256 of t, a dot and the raw body with your whsec_ secret using Web Crypto. Reject the request with a 401 if it doesn't match v1.

What does it cost?

Captions and slideshows cost $0.02 per output minute, plus $0.05 per 1,000 characters when reelsdk writes the voiceover. Faceless shorts are $0.25 up to 60 s and $0.35 up to 90 s, and raw ffmpeg is $0.01 per compute minute. There is no subscription and the first $1 is free. Each key runs 3 jobs at once and up to 500 jobs a day.

All integrations

Ready to build?

Ship video from your app today

Your first $1 of renders is free, and one key covers every endpoint.

Moonlit lake with a lit cabin under the mountains