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
What you hit in Bolt.new

jsh: command not found: ffmpeg

WebContainer CANNOT run native binaries or compile C/C++ code!

Sources: WebContainer issue 1165, bolt.new system prompt

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();
200 with job.outputs.video when the render finishes inside 120 s, 202 with job.id when it doesn't

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

Bolt.new next to reelsdk
LimitBolt.newreelsdk
PreviewWebContainer in the browser, JavaScript and WebAssembly onlyRenders on reelsdk machines with ffmpeg 8.1
Server functionsSupabase Edge Functions, 256 MB and 2 s of CPU per requestUp to 10 min of run time per job
Native binariesNot in the preview, no subprocesses in productionRaw ffmpeg arguments through POST /v1/ffmpeg
Waiting on a renderA pending fetch uses no CPU time?wait= up to 300 s, then GET /v1/jobs/{id}?wait=
Narration and captionsA TTS service and caption timing you wire up yourselfVoiceover 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.

  1. Paste the prompt into Bolt.new

    It tells Bolt to skip the ffmpeg packages, keep photos in a public photos bucket and build make-slideshow and job-status as server functions that read REELSDK_KEY from 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.
  2. Save the key as a secret

    Create a key in the reelsdk dashboard. Click the database icon, open Secrets and add REELSDK_KEY with your rk_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_...
  3. Check make-slideshow

    It sends the photo URLs and the narration to POST /v1/slideshow?wait=120. reelsdk reads the script with the luna voice, 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 });
    });
  4. Check job-status

    When a render runs past 120 s, the page calls job-status with the id. It asks GET /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 is succeeded or failed.

    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.

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