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
Spawning subprocesses is not allowed on Supabase Edge Runtime.
546 Resource Limit: WORKER_RESOURCE_LIMIT
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();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
| Limit | Supabase Edge Functions | reelsdk |
|---|---|---|
| Memory | 256 MB | The encode runs on reelsdk machines, inputs up to 500 MB each |
| CPU time | 2 s per request, async I/O excluded | Up to 10 min of run time per job |
| Wall clock | 150 s on Free, 400 s on paid plans | ?wait= holds up to 300 s, or a webhook calls you back |
| Subprocesses | Not allowed | Raw ffmpeg 8.1 arguments through POST /v1/ffmpeg |
| Bundle size | 20 MB from the CLI, 5 MB from the dashboard | Nothing 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.
Store the key and the webhook secret
Create a key in the reelsdk dashboard to get an
rk_live_API key and itswhsec_webhook secret. Save both as project secrets with the CLI and read them withDeno.env.get. The webhook function has to accept calls without a Supabase JWT, so deploy it with--no-verify-jwtor setverify_jwt = falseunder[functions.reelsdk-webhook]insupabase/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-jwtStart 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=120keeps 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 }); });Verify the webhook and save the mp4
reelsdk signs every delivery with
Reel-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 oft.bodykeyed with yourwhsec_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 withEdgeRuntime.waitUntilso 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.
The same fix on other platforms
All integrationsReady to build?
Ship video from your app today
Your first $1 of renders is free, and one key covers every endpoint.
