Add Video Processing to Your Lovable App

Lovable built the upload screen, the video_jobs table, and the RLS policy in about four minutes. Then you asked it to compress the uploaded file, and the Edge Function either refuses to deploy or returns a cheerful 200 while the output column stays null forever. The video step is the one part of a Lovable app that the scaffolding cannot actually generate.
Quick answer: Lovable video processing cannot run inside the app itself, because Lovable generates a Supabase backend and Supabase Edge Functions run on Deno with no FFmpeg binary, a documented 2-second CPU budget per request, and a 20MB bundle limit. The do-it-yourself route is to host your own container that runsffmpeg -i input.mp4 -c:v libx264 -crf 23 out.mp4and have the Edge Function call it. To skip running an encoder at all, upload to Supabase Storage, create a signed URL, and POST that URL to FFmpeg Micro athttps://api.ffmpeg-micro.com/v1/transcodesfrom the Edge Function, then write the returned output URL back to the row.
Why the generated video code never runs
The video code Lovable writes fails for a runtime reason, not a logic reason: Supabase Edge Functions execute on Deno, and every popular FFmpeg package for JavaScript needs either a native binary or threads that the sandbox does not provide. Supabase publishes the constraints plainly. Maximum CPU time is 2 seconds per request, maximum memory is 256MB, locally bundled functions cap at 20MB, and libraries requiring multithreading are listed as unsupported.
AI scaffolding does not know that. It picks whatever npm suggests, which is one of four things, and each one fails differently.
| What gets generated | What actually happens |
|---|---|
| `fluent-ffmpeg` | Deploys fine, then throws at runtime. It's a wrapper that shells out to an `ffmpeg` binary that isn't on the box. |
| `ffmpeg-static` | Fetches its binary in a postinstall step. Edge Functions have no postinstall and no writable install path. |
| `@ffmpeg/ffmpeg` (ffmpeg.wasm) | The wasm core alone is larger than the 20MB bundle limit, wants SharedArrayBuffer, and would burn the 2-second CPU budget on a 5-second clip. |
| `ffmpeg-kit-react-native` | Retired. Taner Sener's "Saying Goodbye to FFmpegKit" post and issue #1099 on arthenica/ffmpeg-kit confirm it, and the release binaries now return 404. |
That last one matters more than it looks. Vibe-coded apps keep landing on ffmpeg-kit because it was the right answer for years and the training data still says so. FFmpegKitNext, started in mid-2026, is source-only. There is no drop-in install anymore.
Search for video help in Lovable and most of what you'll find is about playback: a managed streaming platform that ingests your file and hands back an adaptive-bitrate player embed. That solves delivery. It does not compress a 400MB phone upload down to something you can afford to store, burn captions into it, or stamp a logo on the corner. Those jobs stay unclaimed, and they're usually the ones the app was built for.
The shape that works
The working architecture keeps bytes out of the Edge Function entirely and treats the function as a dispatcher, not a worker. The file never passes through Deno, which is what makes the 256MB memory ceiling irrelevant.
- The Lovable frontend uploads directly to a Supabase Storage bucket using the
supabase-jsclient already wired into the project. - It inserts a row into
video_jobswith the storage path andstatus = 'queued'. - An Edge Function creates a time-limited signed URL for that object and POSTs it to the video API.
- The API's job id goes back onto the row, and status flips to
processing. - A second function checks the job, writes the output URL to the row, and sets
status = 'done'.
One detail makes step 3 cheap: Supabase's 2-second CPU limit measures actual compute, not async I/O waiting. A fetch that takes 900ms costs you almost nothing against it. What you can't do is sit in the function waiting for a 3-minute transcode, because wall clock is capped at 150 seconds on the free plan and 400 seconds on paid. Dispatch and exit.
The Edge Function that starts the job
This function reads the storage path off the row, signs it for an hour, and submits the transcode. The signed URL is the whole trick, since the API pulls the file over HTTPS directly from Supabase. It's the same pattern covered in Stop downloading from S3. Give FFmpeg a presigned URL., applied to Supabase Storage instead.
// supabase/functions/start-video-job/index.ts
import { createClient } from "jsr:@supabase/supabase-js@2";
Deno.serve(async (req) => {
const { jobId } = await req.json();
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
const { data: job } = await supabase
.from("video_jobs")
.select("storage_path")
.eq("id", jobId)
.single();
const { data: signed } = await supabase.storage
.from("uploads")
.createSignedUrl(job.storage_path, 3600);
const res = await fetch("https://api.ffmpeg-micro.com/v1/transcodes", {
method: "POST",
headers: {
"Authorization": `Bearer ${Deno.env.get("FFMPEG_MICRO_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: [{ url: signed.signedUrl }],
outputFormat: "mp4",
preset: { quality: "medium", resolution: "1080p" },
}),
});
const { jobId: transcodeId } = await res.json();
await supabase
.from("video_jobs")
.update({ transcode_id: transcodeId, status: "processing" })
.eq("id", jobId);
return new Response(JSON.stringify({ transcode_id: transcodeId }), {
headers: { "Content-Type": "application/json" },
});
});
Set the key as a Supabase secret, never as a Vite variable: supabase secrets set FFMPEG_MICRO_API_KEY=sk_.... Anything Lovable names VITE_SOMETHING gets compiled into the browser bundle and is readable by every visitor. That mistake ships to production constantly.
If you want raw FFmpeg control instead of the preset, swap preset for an options array of { "option": "-crf", "argument": "23" } pairs. Signing up takes an email and no credit card, and the free tier is 200 tokens, roughly 33 minutes of video, which is enough to get the whole flow working before you decide anything: start here.
Finishing the job and updating the UI
Completion is a second, much smaller function that reads the job status and, when it's done, grabs a download URL. FFmpeg Micro's status endpoint returns status, outputUrl, and a queue_position, and a separate /download route hands back a signed HTTPS URL you can put in a <video> tag.
// supabase/functions/check-video-job/index.ts
const auth = { "Authorization": `Bearer ${Deno.env.get("FFMPEG_MICRO_API_KEY")}` };
const status = await fetch(
`https://api.ffmpeg-micro.com/v1/transcodes/${transcodeId}`,
{ headers: auth },
).then((r) => r.json());
if (status.status === "completed") {
const dl = await fetch(
`https://api.ffmpeg-micro.com/v1/transcodes/${transcodeId}/download`,
{ headers: auth },
).then((r) => r.json());
await supabase
.from("video_jobs")
.update({ status: "done", output_url: dl.url })
.eq("transcode_id", transcodeId);
}
On the frontend, don't write a polling loop against the API. The row is already the source of truth, so subscribe to it and let Postgres push the change:
supabase
.channel(`job-${jobId}`)
.on("postgres_changes", {
event: "UPDATE",
schema: "public",
table: "video_jobs",
filter: `id=eq.${jobId}`,
}, (payload) => setOutputUrl(payload.new.output_url))
.subscribe();
Something still has to call check-video-job on a schedule. A pg_cron job every 20 seconds over rows where status = 'processing' is the least fragile option, because it survives the user closing the tab. The broader tradeoffs between scheduled checks and push notifications are laid out in Webhooks for long-running video jobs.
The prompt that keeps Lovable from reaching for a native package
The fix at the prompt level is to forbid the failing approach explicitly, because Lovable will otherwise re-add fluent-ffmpeg the next time you ask it to touch the video feature. Paste this into the chat before asking for the feature:
Do not install fluent-ffmpeg, ffmpeg-static, @ffmpeg/ffmpeg, or any
ffmpeg-kit package. Supabase Edge Functions run Deno and cannot execute
native binaries. For video processing: upload the file to Supabase
Storage from the client, insert a row in video_jobs with the storage
path, then call an Edge Function that creates a signed URL with
createSignedUrl() and POSTs it to an external video API. Store the
returned job id on the row. Never block the Edge Function waiting for
the render to finish.
Where this approach isn't the right call
An Edge Function plus a video API is the wrong shape for two jobs in particular. If what you're building is a video library with adaptive-bitrate playback, DRM, and view analytics, a managed streaming and player platform does that better than any transcoding API, including this one. And if your app needs frame-accurate scrubbing preview inside the browser on short clips, ffmpeg.wasm running client-side on a desktop is genuinely reasonable, as long as you accept it won't work on mobile Safari.
One real limit worth knowing before you design around it: FFmpeg Micro's options array does not accept -filter_complex. Multi-input graph work, like layering three overlays with audio mixing in a single pass, needs the composition endpoints rather than raw option pass-through.
Common pitfalls
These come up in roughly this order when people wire the flow for the first time.
- A public bucket URL that returns 400 to the API because the bucket is actually private. Use
createSignedUrl(), and checksigned.signedUrlisn't undefined before you send it. - Storing the signed download URL in the row permanently. It expires. Either re-issue it when the user clicks play, or copy the output back into Supabase Storage once and serve it from there.
- A signed input URL that expires mid-job. One hour is a safe floor for anything over a few minutes of source video.
- Browser recordings that arrive as WebM and break in Safari. Convert them server-side, which is the same pipeline plus
"outputFormat": "mp4". MediaRecorder WebM isn't broken covers the codec details. - Calling the start function directly from the client with the service role key. Route it through the function's own auth and keep the key server-side.
FAQ
Can Supabase Edge Functions run FFmpeg at all?
Supabase Edge Functions cannot run FFmpeg. The Deno runtime has no FFmpeg binary installed, no way to install one, and Supabase documents a 2-second CPU limit and a 256MB memory ceiling that a real transcode would blow through even if the binary existed.
Where should video files live in a Lovable app?
Video files belong in Supabase Storage, uploaded directly from the browser with the supabase-js client that Lovable already configures. Uploading through an Edge Function wastes the 256MB memory budget and the wall clock limit for no benefit.
How long can a Lovable video upload take before the function times out?
The Edge Function itself should finish in under a second, because it only signs a URL and posts a JSON body. Supabase allows 150 seconds of wall clock on the free plan and 400 seconds on paid plans, but the transcode runs outside that window entirely, so a 20-minute source file is not a timeout problem.
Does this work for captions and watermarks, not just compression?
The same dispatch pattern covers captions, watermarks, thumbnails, audio extraction, and FFprobe metadata. Only the request body changes; the Storage upload, the signed URL, and the status row stay identical.
You can have the whole loop running in a Lovable app in about twenty minutes: sign the URL, post the job, update the row. Sign up free and the 200 starter tokens will cover roughly 33 minutes of video, which is more than enough to prove the flow before you point it at real user uploads.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Webhooks for long-running video jobs: reliable pipeline patterns
Webhooks for long running video jobs fail quietly. Use the hint-plus-reconciler pattern: verify signatures, key idempotency on job ID, add a polling backstop.

Choosing a RenderIO alternative for n8n? Look past the node
Looking for a RenderIO alternative for n8n? Compare per-command quotas, clip duration caps, and webhook gating against usage-based FFmpeg API pricing.

FFmpeg Error Messages, Not Exit Codes, Tell You What to Retry
FFmpeg error messages, not exit codes, tell you what broke: a triage table of exact stderr strings, the cause in a pipeline, the fix, and retry or reject.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free