Skip to main content

Secure Backend Proxy

Use this page when you want a production-safe first integration and do not want raw MediaSFU credentials in the frontend.

Why this matters

MediaSFU is a frontend SDK, but the credential boundary should stay on your backend.

In production, the frontend should:

  • collect user intent such as create room or join room
  • call your app backend
  • receive the backend result
  • pass custom createMediaSFURoom and joinMediaSFURoom functions into the SDK

The backend should:

  • hold MEDIASFU_API_USERNAME and MEDIASFU_API_KEY
  • validate the caller and payload
  • forward the request to MediaSFU Cloud or your self-hosted MediaSFU Open server
  • return only the room result to the frontend

MediaSFU Cloud uses the same upstream endpoint for create and join requests: https://mediasfu.com/v1/rooms/.

Your app can still expose separate /api/mediasfu/create-room and /api/mediasfu/join-room routes if that keeps validation, auth, or auditing simpler. The important contract is that both backend routes forward to the same MediaSFU Cloud rooms URL above.

  1. The user signs into your app.
  2. Your frontend calls /api/mediasfu/create-room or /api/mediasfu/join-room.
  3. Your backend adds MediaSFU credentials server-side.
  4. Your backend forwards the request to MediaSFU.
  5. The frontend passes the result into the MediaSFU room flow.

Backend example: Node + Express

This is the smallest production-safe pattern to start from.

import express from 'express';

const app = express();
app.use(express.json());

const mediaSFURoomsUrl =
process.env.MEDIASFU_ROOMS_URL ?? 'https://mediasfu.com/v1/rooms/';

const mediaSFUHeaders = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.MEDIASFU_API_USERNAME}:${process.env.MEDIASFU_API_KEY}`,
};

async function forwardToMediaSFU(url: string, payload: unknown) {
const response = await fetch(url, {
method: 'POST',
headers: mediaSFUHeaders,
body: JSON.stringify(payload),
});

const data = await response.json();
return { ok: response.ok, status: response.status, data };
}

app.post('/api/mediasfu/create-room', async (req, res) => {
try {
const result = await forwardToMediaSFU(mediaSFURoomsUrl, req.body);
res.status(result.ok ? 200 : result.status).json(result.data);
} catch (error) {
res.status(500).json({
error: `Unable to create room: ${(error as Error).message}`,
});
}
});

app.post('/api/mediasfu/join-room', async (req, res) => {
try {
const result = await forwardToMediaSFU(mediaSFURoomsUrl, req.body);
res.status(result.ok ? 200 : result.status).json(result.data);
} catch (error) {
res.status(500).json({
error: `Unable to join room: ${(error as Error).message}`,
});
}
});

Self-hosted variant

If you are using MediaSFU Open instead of MediaSFU Cloud, point the backend at the room endpoint your server exposes:

  • MEDIASFU_ROOMS_URL=http://your-mediasfu-open-host/rooms/

The frontend contract stays the same.

Frontend example: custom room hooks

The React package already exposes the right extension points. The important rule is that these functions call your backend, not MediaSFU directly.

type RoomResult = {
data: Record<string, unknown> | null;
success: boolean;
};

export async function createMediaSFURoom({ payload }: { payload: unknown }): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/create-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

const data = await response.json();
return { data, success: response.ok };
}

export async function joinMediaSFURoom({ payload }: { payload: unknown }): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

const data = await response.json();
return { data, success: response.ok };
}

Frontend example: MediaSFU room setup

import { useState } from 'react';
import { MediasfuGeneric } from 'mediasfu-reactjs';

export function RoomScreen() {
const [sourceParameters, setSourceParameters] = useState<Record<string, unknown>>({});

return (
<MediasfuGeneric
connectMediaSFU={true}
returnUI={false}
noUIPreJoinOptions={{
action: 'create',
eventType: 'conference',
capacity: 10,
duration: 30,
userName: 'Host',
}}
sourceParameters={sourceParameters}
updateSourceParameters={setSourceParameters}
createMediaSFURoom={createMediaSFURoom}
joinMediaSFURoom={joinMediaSFURoom}
/>
);
}

This keeps credentials on the backend while still using the SDK's normal room flow.

Frontend examples by framework

Use the same backend endpoints regardless of frontend framework. Only the client adapter changes.

React

async function createMediaSFURoom({ payload }: { payload: unknown }) {
const response = await fetch('/api/mediasfu/create-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

async function joinMediaSFURoom({ payload }: { payload: unknown }) {
const response = await fetch('/api/mediasfu/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

Angular

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class MediaSfuProxyService {
constructor(private readonly http: HttpClient) {}

createRoom(payload: unknown) {
return this.http
.post('/api/mediasfu/create-room', payload)
.pipe(map((data) => ({ data, success: true })));
}

joinRoom(payload: unknown) {
return this.http
.post('/api/mediasfu/join-room', payload)
.pipe(map((data) => ({ data, success: true })));
}
}

Vue

type RoomResult = { data: unknown; success: boolean };

export async function createMediaSFURoom(payload: unknown): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/create-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

export async function joinMediaSFURoom(payload: unknown): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

For framework-specific component wiring, use the matching SDK page after you confirm the backend boundary is working.

Minimum production checklist

  • Keep MediaSFU credentials in backend environment variables only.
  • Authenticate the user before allowing create or join actions.
  • Validate room payloads before forwarding them.
  • Rate-limit your proxy endpoints.
  • Log MediaSFU failures on the backend, not only in the browser.
  • Treat the frontend createMediaSFURoom and joinMediaSFURoom hooks as thin adapters.

Where to go next