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:
- SSR - render on your own server via API endpoints
- 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 URLThe 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/progressPOST {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 uuidRender State Helper
A simple file-based state store so the render and progress endpoints can share state:
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
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
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:
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) — Default | SSR | Lambda | |
|---|---|---|---|
| Where it runs | User's browser | Your server | AWS Lambda |
| Best for | Getting started, light-medium use | Full control, all browsers | Production, high volume |
| Server setup | None | API routes required | AWS + API routes |
| Scaling | N/A (client device) | Manual | Automatic |
| Browser support | Chrome/Edge only | Any | Any |
| Dependencies | @remotion/web-renderer, @remotion/media | @remotion/bundler, @remotion/renderer | @remotion/lambda |
- Client-Side Rendering - enabled by default, render in the browser, no server needed
- SSR Rendering - optional, render on your own server
- Lambda Rendering - optional, render on AWS Lambda for production scale