RVE LogoReact Video EditorDOCS
RVE SDK/Rendering/Setting Up Server Rendering

Setting Up Server Rendering

How to build optional API endpoints for server-side video rendering

The RVE SDK ships with client-side rendering enabled by default — users can export videos directly in their browser using WebCodecs with zero server setup.

For production workloads, you can optionally add server rendering to handle higher volumes, support all browsers, or offload processing from the client. You have two server rendering options:

  1. SSR - render on your own server via API endpoints
  2. Lambda - render on AWS Lambda for auto-scaling

For server rendering, you provide two API endpoints. The SDK's HttpRenderer calls them automatically via the customRenderer prop.


How It Works

User clicks "Export"


SDK calls POST /api/render/ssr/render
  → Your server starts a Remotion render job
  → Returns { renderId: "uuid" }


SDK polls POST /api/render/ssr/progress
  → Your server returns progress, done, or error
  → SDK shows progress bar to user


Render complete → SDK receives download URL

The API Contract

Your server needs to implement two endpoints. The base path is whatever you pass to HttpRenderer:

// Pass a customRenderer to enable server rendering:
const renderer = new HttpRenderer('/api/render/ssr', { type: 'ssr', entryPoint: '/api/render/ssr' });

<ReactVideoEditor
  projectId="my-project"
  customRenderer={renderer}
  enableWebRender={true}       // keep browser export as fallback
  showRenderSettings={true}    // let users toggle between modes
/>

// The SDK will call:
// POST /api/render/ssr/render
// POST /api/render/ssr/progress

POST {base}/render

Request body:

{
  "id": "composition-id",
  "inputProps": {
    "overlays": [...],
    "durationInFrames": 900,
    "width": 1920,
    "height": 1080,
    "fps": 30
  }
}

Expected response:

{
  "renderId": "unique-render-id"
}

POST {base}/progress

Request body:

{
  "id": "the-renderId-from-above"
}

Expected response (one of three shapes):

{
  "type": "progress",
  "progress": 0.45
}
{
  "type": "done",
  "url": "/rendered-videos/abc123.mp4",
  "size": 12345678
}
{
  "type": "error",
  "message": "Render failed: out of memory"
}

The SDK polls the progress endpoint every second until it receives a done or error response.


Next.js Implementation

Here's a working implementation using Next.js App Router and Remotion SSR.

Install Dependencies

npm install @remotion/bundler @remotion/renderer remotion uuid

Render State Helper

A simple file-based state store so the render and progress endpoints can share state:

app/api/render/ssr/lib/render-state.ts
import fs from 'fs';
import path from 'path';

const RENDER_STATE_DIR = path.join(process.cwd(), 'tmp', 'render-state');

if (!fs.existsSync(RENDER_STATE_DIR)) {
  fs.mkdirSync(RENDER_STATE_DIR, { recursive: true });
}

export const saveRenderState = (renderId: string, state: unknown) => {
  fs.writeFileSync(
    path.join(RENDER_STATE_DIR, `${renderId}.json`),
    JSON.stringify(state)
  );
};

export const getRenderState = (renderId: string) => {
  const filePath = path.join(RENDER_STATE_DIR, `${renderId}.json`);
  if (!fs.existsSync(filePath)) return null;
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
};

export const updateRenderProgress = (renderId: string, progress: number) => {
  const state = getRenderState(renderId) || {};
  state.progress = progress;
  state.status = 'rendering';
  saveRenderState(renderId, state);
};

export const completeRender = (renderId: string, url: string, size: number) => {
  const state = getRenderState(renderId) || {};
  state.status = 'done';
  state.url = url;
  state.size = size;
  saveRenderState(renderId, state);
};

export const failRender = (renderId: string, error: string) => {
  const state = getRenderState(renderId) || {};
  state.status = 'error';
  state.error = error;
  saveRenderState(renderId, state);
};

Render Endpoint

app/api/render/ssr/render/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
import {
  saveRenderState,
  updateRenderProgress,
  completeRender,
  failRender,
} from '../lib/render-state';

const VIDEOS_DIR = path.join(process.cwd(), 'public', 'rendered-videos');
if (!fs.existsSync(VIDEOS_DIR)) {
  fs.mkdirSync(VIDEOS_DIR, { recursive: true });
}

export async function POST(request: NextRequest) {
  try {
    const { id, inputProps } = await request.json();
    const renderId = uuidv4();

    // Save initial state
    saveRenderState(renderId, {
      status: 'rendering',
      progress: 0,
      timestamp: Date.now(),
    });

    // Start rendering asynchronously (don't await)
    (async () => {
      try {
        // Bundle your Remotion composition
        const bundleLocation = await bundle(
          path.join(process.cwd(), 'remotion', 'index.ts')
        );

        // Select the composition
        const composition = await selectComposition({
          serveUrl: bundleLocation,
          id,
          inputProps,
        });

        // Render the video
        await renderMedia({
          codec: 'h264',
          composition: {
            ...composition,
            durationInFrames: inputProps.durationInFrames || composition.durationInFrames,
            width: inputProps.width || composition.width,
            height: inputProps.height || composition.height,
          },
          serveUrl: bundleLocation,
          outputLocation: path.join(VIDEOS_DIR, `${renderId}.mp4`),
          inputProps,
          onProgress: ({ progress }) => {
            updateRenderProgress(renderId, progress);
          },
        });

        const stats = fs.statSync(path.join(VIDEOS_DIR, `${renderId}.mp4`));
        completeRender(renderId, `/rendered-videos/${renderId}.mp4`, stats.size);
      } catch (error) {
        failRender(renderId, (error as Error).message);
      }
    })();

    return NextResponse.json({ renderId });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to start render' },
      { status: 500 }
    );
  }
}

Progress Endpoint

app/api/render/ssr/progress/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getRenderState } from '../lib/render-state';

export async function POST(request: NextRequest) {
  try {
    const { id } = await request.json();
    const renderState = getRenderState(id);

    if (!renderState) {
      return NextResponse.json({
        type: 'error',
        message: `No render found with ID: ${id}`,
      });
    }

    switch (renderState.status) {
      case 'error':
        return NextResponse.json({
          type: 'error',
          message: renderState.error || 'Unknown error',
        });
      case 'done':
        return NextResponse.json({
          type: 'done',
          url: renderState.url,
          size: renderState.size,
        });
      default:
        return NextResponse.json({
          type: 'progress',
          progress: renderState.progress || 0,
        });
    }
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to get render progress' },
      { status: 500 }
    );
  }
}

Remotion Composition

Your render endpoint needs a Remotion composition that knows how to render the SDK's overlay data. The SDK exports layer components you can use in your composition:

remotion/index.ts
import { registerRoot } from 'remotion';
import { MyComposition } from './Composition';

registerRoot(MyComposition);

Remotion setup

Setting up the Remotion composition is covered in detail in the Remotion documentation. The SDK provides layer rendering components (text-layer-content, video-layer-content, image-layer-content, etc.) that you can use inside your composition to render each overlay type.


Rendering Modes

CSR (Browser) — DefaultSSRLambda
Where it runsUser's browserYour serverAWS Lambda
Best forGetting started, light-medium useFull control, all browsersProduction, high volume
Server setupNoneAPI routes requiredAWS + API routes
ScalingN/A (client device)ManualAutomatic
Browser supportChrome/Edge onlyAnyAny
Dependencies@remotion/web-renderer, @remotion/media@remotion/bundler, @remotion/renderer@remotion/lambda