livqenoDocs

MigrateFrom 100ms

Migrate from 100ms

Map 100ms templates, roles, auth tokens, peers, recording, HLS, RTMP and webhooks onto Livqeno, with side-by-side code.

This guide moves a web app from 100ms (@100mslive/hms-video-store or the React SDK on the client, auth tokens and the REST API on the server) to Livqeno. Read the overview first for the general approach.

The biggest modelling change: 100ms puts behaviour in a dashboard template (roles, what each role may publish, recording and streaming settings). Livqeno has no templates. What a user may do is in the token your backend makes, and large-audience and streaming features live on live streams.

Concept mapping

100msLivqenoNotes
App access key + app secretProject + API keyThe API key never leaves your backend.
Management token (for the REST API)The API key, as a bearer tokenNo separate management token to sign.
TemplateNo equivalentRoom options (e2ee) are set when the room is created; everything else is per token.
Room (room_id, created via dashboard or API)Roomlivqeno.rooms.create({ name, getOrCreate: true }), then make tokens for room.id.
Room codes, Prebuilt UINo prebuilt video UIBuild with @livqeno/react components and hooks. Chat has UI components.
Role (host, guest, viewer…)permissions in the tokenjoin, subscribe, publish, publishAudio, publishVideo, publishData.
Change rolePATCH /v1/rooms/{roomId}/participants/{identity}/permissionsOr the stage for live streams.
Auth token (JWT you sign with the app secret)livqeno.tokens.create()Made by the API. Returns a grant with token, endpoint and iceServers.
Peer, user_idParticipant, identity
hmsActions.join({ userName, authToken })createRTCClient(grant).join()
selectPeers + attachVideotrackSubscribed + track.attach()Livqeno is event-based, not a reactive store. React hooks are in @livqeno/react.
setLocalAudioEnabled / setLocalVideoEnabledtrack.mute() / room.enableCamera() / room.disableCamera()
setScreenShareEnabled(true)room.enableScreenShare()Video only.
sendBroadcastMessageroom.sendData(), or Chat for saved messagesNo direct or group (per-role) targeting with sendData().
Simulcast (template setting)On by defaulttrack.setLayer() to pick a quality.
Recording (browser composite, track, SFU)No equivalent for callsSee Gaps.
startRTMPOrRecording({ rtmpURLs })RestreamLive streams only.
startHLSStreaming()A live stream with deliveryMode: 'BROADCAST'See Scaling the audience.
RTMP-in (stream key)IngestWHIP or RTMP(S). Live streams only.
WebhooksWebhooksHMAC-SHA256 signed.
Noise suppression / virtual background pluginsNot available. See Effects removed.
Region (set on the room)Automatic placementSee Regions.
Remove peer, end room (REST)POST /v1/rooms/{roomId}/participants/{identity}/remove, DELETE /v1/rooms/{roomId}

Server: make a token

import jwt from 'jsonwebtoken';
import { randomUUID } from 'node:crypto';
 
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
  {
    access_key: process.env.HMS_ACCESS_KEY,
    room_id: roomId,
    user_id: user.id,
    role: 'host',
    type: 'app',
    version: 2,
    iat: now,
    nbf: now,
  },
  process.env.HMS_APP_SECRET,
  { algorithm: 'HS256', expiresIn: '24h', jwtid: randomUUID() },
);
import { Livqeno } from '@livqeno/server';
 
const livqeno = new Livqeno({ apiKey: process.env.LIVQENO_API_KEY, baseUrl: process.env.LIVQENO_API_URL });
 
// What a 100ms role allowed now lives in the token.
const ROLES = {
  host: { join: true, subscribe: true, publish: true, publishData: true },
  guest: { join: true, subscribe: true, publish: true, publishAudio: true, publishVideo: false },
  viewer: { join: true, subscribe: true },
};
 
const room = await livqeno.rooms.create({ name: roomName, getOrCreate: true });
const grant = await livqeno.tokens.create({
  room: room.id,
  identity: user.id,
  permissions: ROLES.host,
  expiresIn: 3600,
});
res.json(grant); // forward the whole grant: token, endpoint, iceServers

Tokens last at most 6 hours, not 24. Pass refreshToken to createRTCClient for longer sessions (RTC authentication). See Permissions for common permission sets.

Client: join, publish and subscribe

import { HMSReactiveStore, selectPeers } from '@100mslive/hms-video-store';
 
const hms = new HMSReactiveStore();
hms.triggerOnSubscribe();
const hmsActions = hms.getActions();
const hmsStore = hms.getStore();
 
await hmsActions.join({ userName: 'Alice', authToken: token });
 
hmsStore.subscribe((peers) => {
  for (const peer of peers) {
    if (peer.videoTrack) hmsActions.attachVideo(peer.videoTrack, videoElementFor(peer.id));
  }
}, selectPeers);

With 100ms, the template decides whether a role publishes on join. With Livqeno nothing is published until your app calls enableCamera() / enableMicrophone(). hmsActions.leave() becomes await room.leave().

Mute and unmute

await hmsActions.setLocalAudioEnabled(false);
await hmsActions.setLocalAudioEnabled(true);

A host muting someone else (100ms setRemoteTrackEnabled) is a backend call in Livqeno: POST /v1/rooms/{roomId}/participants/{identity}/mute with { "kind": "audio", "muted": true }. Only your API key can do it, so add a backend endpoint that checks the caller is a host.

Screen share

await hmsActions.setScreenShareEnabled(true);
await hmsActions.setScreenShareEnabled(false);

Messages

await hmsActions.sendBroadcastMessage('hello');
// Also: sendDirectMessage(message, peerId), sendGroupMessage(message, roles)

100ms room messages last only for the session. Livqeno's sendData() is the same (and reaches only people connected at that moment). For a chat panel with history, use Chat and attach the conversation to the call with roomId when you create it. Hand raise, polls and other "session store" patterns become sendData() messages that your app interprets.

Recording, HLS and RTMP

// Browser-composite recording and/or RTMP out, started from the client
await hmsActions.startRTMPOrRecording({
  meetingURL,
  rtmpURLs: [`rtmp://a.rtmp.youtube.com/live2/${youtubeKey}`],
  record: true,
});
await hmsActions.stopRTMPAndRecording();
 
// HLS for a large audience
await hmsActions.startHLSStreaming();
await hmsActions.stopHLSStreaming();

The host joins with LiveStream.join(hostGrant) from @livqeno/live and turns on camera and mic on stream.room. Restream and HLS can't be started from a client, and there is no recording of an ordinary room. See Gaps.

RTMP-in maps to livqeno.liveStreams.ingress.create(stream.id, { protocol: 'RTMP' }), which returns rtmpsUrl and a streamKey for OBS. See OBS & encoder ingest.

Webhooks

// 100ms sends the custom headers you configure in its dashboard with each
// webhook; you check them yourself (check their docs for the current options).
if (req.get('x-webhook-secret') !== process.env.HMS_WEBHOOK_SECRET) return res.sendStatus(401);
const { type, data } = req.body;
if (type === 'peer.join.success') {
  // data.room_id, data.user_id
}

Every server SDK ships the same check (verifyWebhook in TypeScript, verify_webhook in Python, livqeno.VerifyWebhookRequest, WebhookVerifier.verify, Webhook::constructEvent). See Webhooks.

100ms webhook (check their docs for the full list)Livqeno webhook
session.open.success / session.close.successroom.started / room.ended
peer.join.success / peer.leave.successroom.participant_joined / room.participant_left
Track add / remove eventsroom.track_published / room.track_unpublished
Recording eventslive_stream.recording_ready for broadcast-mode streams only
HLS eventslive_stream.broadcast_ready, live_stream.egress_failed
RTMP-out eventslive_stream.restream_active / live_stream.restream_failed
RTMP-in eventslive_stream.ingress_connected / _disconnected / _failed

Feature gaps

What 100ms has that Livqeno doesn't, as of today:

100ms featureOn Livqeno
Templates and dashboard-defined rolesNot available. Permissions are set per token and can be changed per participant by your backend.
Prebuilt video UI, room codesNot available. Build the UI with @livqeno/react.
Browser-composite, track and SFU recordingNot available for calls. Broadcast-mode live streams get an HLS replay.
Recording to your own storage bucketNot available.
Starting HLS, RTMP or recording from the clientNot available. Your backend starts live streams and restreams.
HLS or RTMP on an ordinary roomRestream, HLS and ingest work on live streams only.
Per-role messagessendData() can target identities (destinationIdentities) but not roles. Keep your own role-to-identity list, or use Chat conversations.
Polls, whiteboard, session storeNot available as features. Build them on sendData() or Chat.
Transcription and AI summariesNot available.
E2EE on React NativeNot yet.

Migration checklist

  • Inventory. List every 100ms template and role, every hmsActions call your app makes, your token endpoint, your REST calls (rooms, recordings, live streams) and your webhook handler.
  • Roles → permission sets. Write down, for each role, which of publish, publishAudio, publishVideo, publishData, subscribe it needs. Viewer-only roles at scale become live-stream viewers.
  • Credentials. Create a Livqeno project and an API key per environment (API credentials).
  • Token service. Replace the JWT you sign with rooms.create({ name, getOrCreate: true }) + tokens.create(). Return the whole grant. Add refreshToken for sessions over 6 hours.
  • Client. Replace the reactive store with event handlers (or @livqeno/react hooks). Pick the provider per room.
  • Host controls. Move remote mute, role change and remove-peer to backend endpoints that call the Livqeno REST routes.
  • Streaming. Move HLS, RTMP-out and RTMP-in rooms to live streams. Keep rooms that need recording on 100ms until you have a plan for them.
  • Webhooks. Register a Livqeno endpoint, verify Swalvo-Signature, deduplicate on Livqeno-Event-Id, map events with the table above.
  • Dual-run, then cut over. Ramp new rooms to Livqeno while watching observability. Remove 100ms once no room has used it for a full token lifetime.

Pricing

Livqeno's rates are on Usage. Compare them with 100ms's current pricing page; this guide doesn't quote it.

Next steps