LevelChatDocs
Docs
Migrate from Twilio Video

Migrate from Twilio Video

One CLI run ports the bulk of a twilio-video integration before its December 2026 EOL.

Twilio shut down Programmable Video; its end-of-life is December 2026. The levelchat CLI ports the bulk of a twilio-video integration to @levelchat/web in a single run — and it's free whether you pick LevelChat or not.

~
npx levelchat migrate twilio

What it does

migrate twilio walks your source tree, finds every file that imports twilio-video, applies a set of named, idempotent rewrites, and prints a unified diff of what would change. It does not touch your files until you ask it to.

~
# 1. Dry run — review the diff
npx levelchat migrate twilio

# 2. Apply the rewrites in place
npx levelchat migrate twilio --write

# 3. Or scan just one subtree
npx levelchat migrate twilio --cwd ./packages/app/src

The dry-run output is git apply-compatible, so you can also do:

~
npx levelchat migrate twilio > twilio-migration.patch
# review twilio-migration.patch, then:
git apply twilio-migration.patch

The API mapping

Twilio VideoLevelChat
import Video from 'twilio-video'import { LevelChat } from '@levelchat/web'
import { connect } from 'twilio-video'import { LevelChat } from '@levelchat/web'
require('twilio-video')require('@levelchat/web')
Video.connect(token, opts)new LevelChat().joinRoom({ token })
connect(token, opts)new LevelChat().joinRoom({ token })
room.disconnect()room.leave()
room.participants (Map)room.participants() (iterable)
participant.videoTracks / .audioTracksparticipant.tracks
'participantConnected''participant.joined'
'participantDisconnected''participant.left'
'trackSubscribed''track.subscribed'
'trackUnsubscribed''track.unsubscribed'
'dominantSpeakerChanged''speaker.changed'
'disconnected''room.disconnected'
'reconnecting' / 'reconnected''room.reconnecting' / 'room.reconnected'

What you still do by hand

The CLI is regex-based, not a full AST transform — on purpose. The output is a diff a human reviews, not a silent rewrite. After running it, the tool prints a Manual follow-ups block flagging the call sites that need you:

  • Token endpoint — LevelChat mints JWTs on your server, exactly like Twilio. Keep your token endpoint; joinRoom({ token }) only needs the JWT, not Twilio's options object. See the Quickstart for the token-minting snippet.
  • Local tracksVideo.createLocalVideoTrack() / createLocalAudioTrack() map to joinRoom() options + room.publishCamera() / room.publishMic(). Review these call sites by hand.
  • UI components — if you were rendering Twilio tracks into your own <video> elements, consider swapping in the @levelchat/react-components kit — <VideoTile>, <ParticipantGrid>, <ControlBar> are drop-in.

A before / after

TypeScript
import Video from 'twilio-video';

export async function joinCall(token: string) {
  const room = await Video.connect(token, { audio: true, video: true });
  room.on('participantConnected', (p) => console.log('joined', p.identity));
  room.on('disconnected', () => console.log('bye'));
  for (const participant of room.participants.values()) {
    participant.videoTracks.forEach((t) => attach(t));
  }
  return () => room.disconnect();
}
TypeScript
import { LevelChat } from '@levelchat/web';

export async function joinCall(token: string) {
  const room = await new LevelChat().joinRoom({ token: token });
  room.on('participant.joined', (p) => console.log('joined', p.identity));
  room.on('room.disconnected', () => console.log('bye'));
  for (const participant of room.participants().values()) {
    participant.tracks.forEach((t) => attach(t));
  }
  return () => room.leave();
}

Migrating from something other than Twilio?

The CLI's migrator registry is open for extension. Agora, Daily, and Vonage migrators are on the roadmap — if you need one sooner, open an issue with a couple of representative files and we'll prioritise it.