Production Room Credential Boundary
Use this page before a build leaves local development. It shows how to keep a reusable MediaSFU Cloud credential out of code that users can download or inspect.
Local development versus release
For an isolated local prototype, you may use an SDK's direct create/join helper with a restricted, revocable development credential and test rooms. Never commit that credential, paste it into documentation, or reuse it for real customers.
Before you distribute the application, open testing to people outside the development team, or connect production rooms, move the reusable credential to your authenticated application backend. The client then sends room intent and receives an authorized room result. You do not need to build this boundary before experimenting locally, but it is a release requirement.
Why this matters
MediaSFU supplies frontend SDKs, but a released client's reusable Cloud credential belongs 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
createMediaSFURoomandjoinMediaSFURoomfunctions into the SDK
The backend should:
- hold
MEDIASFU_API_USERNAMEandMEDIASFU_API_KEY - validate the caller and payload
- validate and forward an
Idempotency-Keyfor create and join retries - 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/.
Optional server-side room audio denoising belongs on an authorized create request (or its meetingRoomParams), not on join. The backend checks account eligibility and capability availability.
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.
Release request flow
- The user signs into your app.
- Your frontend calls
/api/mediasfu/create-roomor/api/mediasfu/join-room. - Your backend adds MediaSFU credentials server-side.
- Your backend forwards the request to MediaSFU.
- 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 idempotencyKeyPattern = /^[\x21-\x7E]{8,128}$/;
function idempotencyKeyFrom(req: express.Request) {
const value = req.get('Idempotency-Key');
if (value === undefined) return undefined;
if (!idempotencyKeyPattern.test(value)) throw new Error('Invalid Idempotency-Key.');
return value;
}
async function forwardToMediaSFU(url: string, payload: unknown, idempotencyKey?: string) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.MEDIASFU_API_USERNAME}:${process.env.MEDIASFU_API_KEY}`,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
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, idempotencyKeyFrom(req));
res.status(result.ok ? 200 : result.status).json(
result.ok
? { success: true, data: result.data }
: { success: false, error: result.data?.error ?? 'Create failed.' },
);
} 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, idempotencyKeyFrom(req));
res.status(result.ok ? 200 : result.status).json(
result.ok
? { success: true, data: result.data }
: { success: false, error: result.data?.error ?? 'Join failed.' },
);
} catch (error) {
res.status(500).json({
error: `Unable to join room: ${(error as Error).message}`,
});
}
});
Self-hosted variant
MediaSFU Open is a media server that you deploy, run, secure, and monitor. localLink or a backend URL only points at that running server; it does not start one. 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. Keep one idempotency key for the logical action: reuse it after a network error or retryable server response, then clear it after success or a client error. A new create or join gets a new key.
type RoomResult = {
data: Record<string, unknown> | null;
success: boolean;
};
function retrySafeRoomRequest(path: string) {
let retryKey: string | undefined;
return async ({ payload }: { payload: unknown }): Promise<RoomResult> => {
retryKey ??= crypto.randomUUID();
const response = await fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': retryKey },
body: JSON.stringify(payload),
});
const body = await response.json();
if (response.ok || response.status < 500) retryKey = undefined;
return response.ok && body.success !== false
? { data: body.data, success: true }
: { data: { error: body.error ?? 'Room request failed.' }, success: false };
};
}
export const createMediaSFURoom = retrySafeRoomRequest('/api/mediasfu/create-room');
export const joinMediaSFURoom = retrySafeRoomRequest('/api/mediasfu/join-room');
Frontend example: MediaSFU room setup
import { useState } from 'react';
import { ModernMediasfuGeneric } from 'mediasfu-reactjs';
const clientPlaceholderCredentials = {
apiUserName: 'client00',
apiKey: '0'.repeat(64),
};
export function RoomScreen() {
const [sourceParameters, setSourceParameters] = useState<Record<string, unknown>>({});
return (
<ModernMediasfuGeneric
credentials={clientPlaceholderCredentials}
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.
The placeholders satisfy the SDK's input shape; they are not authentication. Both callbacks must be present, both must ignore the callback credential values, and your backend must authenticate the app user before substituting real MediaSFU credentials. If either callback is missing, that path can fall back to the default client credential request.
Frontend adapter examples
Use the same backend endpoints regardless of SDK or platform. 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 SDK-specific component wiring, use the matching SDK page after you confirm the backend boundary is working. Flutter, Kotlin, Swift, Unity, and other non-web clients should follow the same backend boundary with their own networking layer and then pass the results into the SDK-specific room hooks or launch config.
Native integration notes
- Flutter: replace
createMediaSFURoomandjoinMediaSFURoominMediasfuGenericOptionswhen your app should call your backend instead of direct cloud helpers. - Kotlin: let your backend handle auth, room policy, and privileged credentials before you mount the Compose room. Keep the client focused on UI flow and runtime state.
- Swift: let your backend validate or prepare room details first, then hydrate the
MediaSFUIosHostBridgelaunch config your app presents. - Unity: let your app or game perform auth and room selection first, then call
JoinRoomAsyncwith the room details your backend approves.
Flutter example: swap in backend-backed room helpers
ModernMediasfuGeneric(
options: ModernMediasfuGenericOptions(
credentials: clientPlaceholderCredentials,
createMediaSFURoom: createMediaSFURoom,
joinMediaSFURoom: joinMediaSFURoom,
),
)
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.
- Forward a validated
Idempotency-Key; reuse it only for an exact create/join retry. - Rate-limit your proxy endpoints.
- Log MediaSFU failures on the backend, not only in the browser.
- Treat the frontend
createMediaSFURoomandjoinMediaSFURoomhooks as thin adapters. - Inject both callbacks and verify neither forwards callback credential fields.
- Exercise the request shape first in the REST API Sandbox, create or rotate credentials at API Keys, and confirm the room API contract in the Developer Console guide.
Where to go next
- Need help deciding how much UI to keep? Read the Build Style Guide.
- Need custom runtime control after join works? Read Media Lifecycle.
- Want product-shaped examples after the secure path is working? Read Starter Screens.