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 twilioWhat 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/srcThe 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.patchThe API mapping
| Twilio Video | LevelChat |
|---|---|
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 / .audioTracks | participant.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 tracks —
Video.createLocalVideoTrack()/createLocalAudioTrack()map tojoinRoom()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-componentskit —<VideoTile>,<ParticipantGrid>,<ControlBar>are drop-in.
A before / after
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();
}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.