Bolt.new + reelsdk
Turn photos into a narrated video from a Bolt.new app
Bolt.new previews your app in a WebContainer that can't run native binaries, and Bolt Cloud runs your server functions on Supabase Edge Functions with 256 MB of memory and 2 s of CPU per request. reelsdk renders on its own machines: one call turns photos and a script into a 9:16 slideshow with voiceover and karaoke captions, ready in 40 to 45 s for a 30 s video, at $0.02 per output minute plus $0.05 per 1,000 characters of narration.
- A 30 s narrated slideshow renders in 40 to 45 s
- About $0.03 for 30 s with 450 characters of narration
- Your key lives in Bolt secrets, never in the browser
jsh: command not found: ffmpeg
WebContainer CANNOT run native binaries or compile C/C++ code!
The same job as one call to reelsdk
const REELSDK = "https://api.reelsdk.com";
const res = await fetch(`${REELSDK}/v1/slideshow?wait=120`, {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("REELSDK_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
images,
voiceover: { text: narration },
captions: "karaoke",
}),
});
const { job } = await res.json();Why ffmpeg breaks here
A browser sandbox in the editor, an edge function in production
Bolt.new runs your project in a StackBlitz WebContainer, a Node.js environment that lives inside the browser tab. It runs JavaScript and WebAssembly, not native programs, so ffmpeg, ffmpeg-static and fluent-ffmpeg have no binary to start and the shell answers jsh: command not found: ffmpeg. Bolt's own system prompt says it plainly: WebContainer CANNOT run native binaries or compile C/C++ code!
In production, Bolt Cloud backs every project with Supabase, and server functions run as Supabase Edge Functions: 256 MB of memory, 2 s of CPU per request and no subprocesses. ffmpeg.wasm does run in the browser, but every visitor downloads a core of about 31 MB and encodes on their own device, which crawls on phones and stops when the tab closes.
Bolt.new next to reelsdk
| Limit | Bolt.new | reelsdk |
|---|---|---|
| Preview | WebContainer in the browser, JavaScript and WebAssembly only | Renders on reelsdk machines with ffmpeg 8.1 |
| Server functions | Supabase Edge Functions, 256 MB and 2 s of CPU per request | Up to 10 min of run time per job |
| Native binaries | Not in the preview, no subprocesses in production | Raw ffmpeg arguments through POST /v1/ffmpeg |
| Waiting on a render | A pending fetch uses no CPU time | ?wait= up to 300 s, then GET /v1/jobs/{id}?wait= |
| Narration and captions | A TTS service and caption timing you wire up yourself | Voiceover from text and karaoke captions in the same call |
Bolt.new limits from Supabase Edge Functions docs, checked October 2026.
Set it up
One prompt, two server functions, no webhook to host
You need a reelsdk key. The prompt below has Bolt.new build the upload form, a server function that starts the render and one that checks on it. This setup waits and polls instead of receiving a webhook, so there is no public endpoint to secure.
Paste the prompt into Bolt.new
It tells Bolt to skip the ffmpeg packages, keep photos in a public
photosbucket and buildmake-slideshowandjob-statusas server functions that readREELSDK_KEYfrom secrets.Prompt for Bolt.new Add a "Make video" feature that renders on the server with reelsdk (https://reelsdk.com). Don't install ffmpeg, ffmpeg-static, fluent-ffmpeg or ffmpeg.wasm. 1. Ask me for a secret named REELSDK_KEY and store it in the project's secrets. Never use it in frontend code. 2. Let the user upload 3 to 20 photos to a public storage bucket called "photos" and type a short narration. 3. Create a server function "make-slideshow" that takes { image_urls, narration } and POSTs to https://api.reelsdk.com/v1/slideshow?wait=120 with the header "Authorization: Bearer REELSDK_KEY" and the JSON { "images": image_urls, "size": "9:16", "transition": "fade", "voiceover": { "text": narration, "voice": "luna" }, "captions": "karaoke" }. The response is JSON { job }. Return { id: job.id, status: job.status, video: job.outputs.video }. 4. Create a server function "job-status" that takes { id } and calls GET https://api.reelsdk.com/v1/jobs/{id}?wait=25 with the same header, returning the same shape. 5. In the UI, show a progress state while rendering. If status is not "succeeded" yet, call job-status until it is "succeeded" or "failed". Show the mp4 in a video player with a download button.Save the key as a secret
Create a key in the reelsdk dashboard. Click the database icon, open Secrets and add
REELSDK_KEYwith yourrk_live_key. Server functions read secrets when they run and users never see them. If Bolt asks for the secret in chat, its message links straight to that tab.Bolt secrets - REELSDK_KEY
- rk_live_...
Check make-slideshow
It sends the photo URLs and the narration to
POST /v1/slideshow?wait=120. reelsdk reads the script with thelunavoice, times each photo to the narration, crossfades between them and burns karaoke captions. A 30 s video usually comes back inside the wait with the mp4 link, otherwise the function returns the job id.make-slideshow const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: cors }); const { image_urls, narration } = await req.json(); const res = await fetch("https://api.reelsdk.com/v1/slideshow?wait=120", { method: "POST", headers: { Authorization: `Bearer ${Deno.env.get("REELSDK_KEY")}`, "Content-Type": "application/json", }, body: JSON.stringify({ images: image_urls, size: "9:16", transition: "fade", voiceover: { text: narration, voice: "luna" }, captions: "karaoke", }), }); const { job, error } = await res.json(); if (!res.ok) return Response.json({ error }, { status: res.status, headers: cors }); return Response.json({ id: job.id, status: job.status, video: job.outputs?.video ?? null }, { headers: cors }); });Check job-status
When a render runs past 120 s, the page calls
job-statuswith the id. It asksGET /v1/jobs/{id}?wait=25, which holds the request until the job finishes or 25 s pass, so the browser only asks again when it has to. The page stops when the status issucceededorfailed.job-status const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: cors }); // We long-poll for up to 25 s so the browser asks again only when needed const { id } = await req.json(); const res = await fetch(`https://api.reelsdk.com/v1/jobs/${encodeURIComponent(id)}?wait=25`, { headers: { Authorization: `Bearer ${Deno.env.get("REELSDK_KEY")}` }, }); const { job, error } = await res.json(); if (!res.ok) return Response.json({ error }, { status: res.status, headers: cors }); return Response.json({ id: job.id, status: job.status, video: job.outputs?.video ?? null }, { headers: cors }); });
FAQ
Video on Bolt.new, answered
Why do I get jsh: command not found: ffmpeg in Bolt.new?
Bolt.new runs your project in a StackBlitz WebContainer inside the browser, which runs JavaScript and WebAssembly but not native programs. ffmpeg, ffmpeg-static and fluent-ffmpeg all need a native binary. Deployed server functions run on Supabase Edge Functions, which don't allow subprocesses either, so the render has to happen somewhere else.
Should I use ffmpeg.wasm in a Bolt.new app?
For a desktop tool that trims short clips, it's fine. Every visitor downloads a core of about 31 MB and encodes with their own CPU and battery, and the render stops when the tab closes. For narrated videos, captions or anything people use on phones, render on a server.
Can I get a webhook instead of polling?
Yes, if your backend has a public URL. Pass webhook_url with ?wait=0 and reelsdk POSTs the finished job, signed with a Reel-Signature header, with up to 3 attempts. Bolt doesn't document public URLs for Bolt Cloud server functions, so this setup polls. If your Bolt project uses your own Supabase project, follow the Supabase guide for the webhook version.
How long does a render take, and what does it cost?
A 30 s 1080x1920 slideshow with voiceover and captions renders in 40 to 45 s. It costs $0.02 per output minute plus $0.05 per 1,000 characters of narration, so 30 s with 450 characters comes to about $0.03. There is no subscription and the first $1 is free.
Can I run my own ffmpeg command from a Bolt.new app?
Yes. Send your ffmpeg 8.1 arguments from a server function to POST /v1/ffmpeg. There is no shell, the machine is ready in about 1.9 s at the median and a job can run for up to 10 minutes, at $0.01 per compute minute. Each key runs 3 jobs at once and up to 500 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.
