How to Add LiveKit Video Calling to a Next.js App

How to Add LiveKit Video Calling to a Next.js App

Software Development
September 11, 2026
12-14 min

Share blog

Introduction

We needed video and audio calls inside the product not another Zoom link. This is how we compared LiveKit with Twilio, Daily, Agora, and Zoom, then wired LiveKit Cloud into Next.js and a Laravel API.

Most products do not have a video problem. They have a meeting-link problem.

A customer books a session in your app. You paste a Zoom or Google Meet URL into an email. They leave your product, wait in someone else’s lobby, and when the call ends you still do not own the recording, the attendance, or the next click. That is fine for an internal standup. It is a leak if calling is part of the product.

We closed that leak with LiveKit Cloud: a Next.js client for the call UI, our existing Laravel API for users and permissions, and LiveKit for the media. This post is both the decision and a working map of the integration enough that another developer could follow the same shape in their own stack.

"A third-party meeting link is a meeting in someone else’s product. Embedded video is a feature of yours."

What this post covers

  • Why product teams embed video calling instead of sending people to Zoom or Meet
  • How LiveKit compares with Daily, Agora, Twilio, Vonage, Amazon Chime, Zoom, and self-hosted SFUs
  • Cloud vs self-hosted LiveKit and why we started on Cloud
  • The terms you will hit in code: SFU, adaptive streaming, dynacast, rooms, and tokens
  • The architecture and the actual snippets we used to join a call, handle audio vs video, guests, and recording
  • How LiveKit bills participant-minutes (a 1-minute call with 5 people is 5 minutes)

Why embed video calling instead of a Zoom link?

The fastest way to ship "video" is a third-party URL. Users already know Zoom, Meet, and Teams. Support already knows how to talk people through them. The cost of that convenience shows up later.

  • You lose the session. Notes, files, billing, and the next action now live in another app with another brand.
  • You lose access control. Your API already knows who paid, who is the host, and who is a guest. A shared meeting link does not.
  • You lose the recording. It sits in the vendor’s history instead of next to the rest of the customer’s record.
  • You lose conversion. Open Zoom, sign in, wait for the host every extra click is a drop-off.

An embedded video and audio call keeps the person inside the product. They are already logged in. The UI looks like the rest of the app. When the call ends, they are still on your page.

Building that from raw WebRTC is a different company: media servers, TURN relays, reconnects, recordings. We did not want to become that company. We wanted native calling in Next.js, with the backend we already had still in charge of who may join. That is the gap a platform like LiveKit is for. This is the kind of feature our web application development services are built for.

LiveKit vs Zoom, Twilio, Daily, Agora, and the rest

The 2026 shortlist for "put video calling in our app" is short. We did not pick a vendor from a homepage. We scored escape hatch, developer experience, cost shape for 1:1 and small groups, and whether recordings could land in storage we already own.

PlatformSelf-host pathRough cost shapeBest when
LiveKitYes - same SDK$0.0004–$0.0005 / participant-min + bandwidthEmbedded product, option to self-host later
Daily.coWeak~$0.004 / participant-minFastest embed; you will not run the SFU
AgoraNo - proprietaryAudio looks cheap; HD rates climbGlobal mesh, especially APAC
Twilio VideoNo - closed SFUCPaaS video SKUYou already buy Twilio SMS/voice
Vonage VideoNo - closed SFU~$0.0041 / minuteEnterprise drop-in (old TokBox lineage)
Amazon Chime SDKAWS only~$0.0017 / attendee-minEverything else is already on AWS
Zoom Video SDKNo - Zoom productZoom Video SDK pricingYou want Zoom’s brand and large-session muscle
mediasoup / Janus / JitsiYou run itCompute + peopleYou want to be in the media-server business

How we compared embedded video platforms in 2026. Prices move - use the shape, not a quote.

Daily.co

Daily is a pleasure to integrate and bills a simple ~$0.004 per participant-minute after a free bucket. If the goal is a call on screen by lunch, it wins. It is a weaker escape hatch if you later want the media server under your own roof.

Agora

Agora has a serious global mesh, especially in APAC. Audio looks cheap; HD and Full-HD published rates climb quickly. The stack is proprietary. Fine if you are buying coverage. Less fine if you want an open protocol.

Twilio Video and Vonage Video

This is the familiar CPaaS path. We already used Twilio for SMS, which makes "just add Video" tempting. Video is a different product and a different bill. Vonage the old TokBox lineage is the closer enterprise drop-in, around $0.0041 per minute. Both are closed SFUs.

Amazon Chime SDK

Attractive if everything else is already on AWS: roughly $0.0017 per attendee-minute, one invoice, a clearer HIPAA story. The developer experience and UI kits lag the specialists.

Zoom Video SDK

This embeds Zoom. Fine if you want Zoom’s brand and large-session muscle. You are building on Zoom’s product, not an open protocol you could later host yourself.

mediasoup, Janus, or Jitsi

You are in the media-server business. That is a valid choice. It was not ours. We needed to ship calling this quarter, not hire an SRE rotation for one-way audio at 2 a.m.

Why LiveKit won

We picked LiveKit because the trade-offs matched a product team, not a telecom team:

  • Cloud and self-hosted speak the same protocol. We could ship on *.livekit.cloud without a vendor-only client API. If data residency or unit economics later demand it, the same Next.js code can point at a server we operate.
  • The React kit is a starting point, not a prison. Stock components got a conference on screen quickly. We replaced the chrome without rewriting WebRTC.
  • Grants matched the product. An audio call is a publish-source list. Guests are a different identity. Hosts and guests join the same room.
  • Recordings land in our bucket. Cloud egress writes a grid layout to the S3 we already use.
  • The cost shape fits 1:1 and small groups. Connection minutes in the $0.0004–$0.0005 range plus bandwidth as long as we watch HD send and recording.
  • Agents and SIP can land in the same rooms later. Without a second video vendor.

We did not pick it to be the cheapest SFU at a hundred million minutes, or the fastest "iframe a call by lunch" widget. Daily or Whereby would win the latter. A self-hosted SFU would win the former once someone is paid to run it.

Cloud LiveKit or a server you run yourself

LiveKit is an open-source realtime media server with client SDKs for web and mobile. You can run it two ways. The client code is almost the same in both - that is the point.

Self-hosted means you operate livekit-server, usually on Kubernetes or a set of VMs, plus TURN for people behind strict firewalls. The software is Apache 2.0. You pay for compute, bandwidth, and the people who keep it up. If you already own that scale, our DevOps services run Kubernetes workloads day to day. At very high volume that can beat any vendor’s per-minute invoice.

LiveKit Cloud is the same protocol, managed. You get a project WebSocket URL, an API key and secret, a global edge network, and dashboards. You do not run the SFU. You pay a monthly plan plus usage.

Start on Cloud. If you later need to self-host, you change the server URL - not the client architecture. We were not trying to become a media-platform company, so Cloud was the honest first step.

How LiveKit works: SFU, adaptive streaming, and dynacast

Before the snippets, three terms. Skip this and the Next.js options={{ adaptiveStream: true, dynacast: true }} line looks like magic. It is not. It is how you keep a five-person grid from costing like five full-HD broadcasts.

What is an SFU?

SFU means Selective Forwarding Unit - a media server in the middle of the call. WebRTC is the browser standard for live audio and video. Two people on a friendly network can talk peer-to-peer. Groups break that model: every extra person is another upload, more CPU, and another firewall problem.

With an SFU, each person uploads camera and microphone once. The server forwards those streams to everyone else. Without it, a five-person call means each laptop sending video to four other people. LiveKit is that SFU, plus signaling: who is in the room, which tracks exist, and what each person may publish.

What is adaptive streaming?

Adaptive streaming is how the client matches video quality to the size of the tile on screen. If someone is a small thumbnail, the browser asks for a small stream. A tiny tile does not need 720p, so LiveKit does not send 720p into it.

What is dynacast?

Dynacast is how the publisher and the SFU stop encoding quality layers that nobody is subscribed to. If no one is watching your camera at full size, you do not send a full-size stream. Adaptive streaming is the request. Dynacast is the decision not to produce unused layers.

Turn both on in the LiveKit room options. They are the difference between a demo and a bill you can live with.

Room, participant, track, token

  • Room - the named session. It appears when the first valid token joins. You do not have to pre-create it for the happy path.
  • Participant - a connected client with an identity, such as user-42 or guest-….
  • Track - one stream: camera, microphone, or screen share.
  • Token - a short-lived JWT. It is not your app login. It only says this identity may join this room, with these grants.

The architecture: Next.js, your API, LiveKit Cloud

Keep two jobs separate. Your API already knows who is logged in and who is allowed into a video call or audio call. LiveKit only needs a short-lived token that says "this identity may join this room." The browser never needs the LiveKit API secret. It needs a token and a WebSocket URL.

Let's Build Something Great Together

Ready to transform your idea into a powerful software solution? Talk to our experts and get a free consultation.

Contact Us
  • 1
    The product creates a session and a unique room name, for example call-{uuid}.
  • 2
    When someone is allowed to join, the API signs a JWT and returns { token, server_url, room_name, identity }.
  • 3
    Next.js passes the token and URL into LiveKit’s LiveKitRoom component.
  • 4
    WebRTC talks to wss://….livekit.cloud. Audio and video do not transit your app servers.

App login - or a public guest link answers who the person is to you. The LiveKit token answers whether they may enter this room. Those are two different questions, so they stay in two different places.

The Cloud URL is public on purpose. It is a host, not a secret. We still return server_url with every token so the client does not have to guess, and we also expose it as NEXT_PUBLIC_LIVEKIT_URL so the client can warm DNS and TLS before the user hits Join.

How we integrated LiveKit (the parts that matter)

On the client: @livekit/components-react and livekit-client. On the API: the PHP server SDK for room and recording calls. Tokens are ordinary HS256 JWTs we sign ourselves. File names below are generic video call, audio call not product-specific routes.

1. Install the SDKs

json
package.json · JavaScript
"@livekit/components-react": "^2.9.21",
"@livekit/components-styles": "^1.2.0",
"livekit-client": "^2.19.2"

2. Keep credentials on the server

Cloud setup is three environment values. Put them next to the rest of your API config. Recording, if you want it, is extra config that points egress at object storage.

php
config/livekit.php · PHP
return [
'url' => env('LIVEKIT_URL'),
'api_key' => env('LIVEKIT_API_KEY'),
'api_secret' => env('LIVEKIT_API_SECRET'),
'token_ttl' => (int) env('LIVEKIT_TOKEN_TTL', 3600),
];

3. One token shape for every join path

Accept, reconnect, host join, guest join all of them should return the same payload. The UI never builds JWTs. It only asks the API for a token.

typescript
app/services/calls.ts · TypeScript
export type LiveKitTokenResponse = {
token: string;
server_url: string;
room_name: string;
identity: string;
};
export async function fetchCallToken(callId: string) {
const response = await apiClient.get<{
success: boolean;
data: LiveKitTokenResponse;
}>(`calls/${callId}/token`);
return response.data.data;
}

4. Create the room, then mint the token

Generate the room name yourself. LiveKit does not need a pre-create call for the happy path; the room materializes on first join. An audio call is the same room as a video call. You simply omit camera from canPublishSources. Screen share can still be allowed. That is a permission tweak, not a second product.

php
app/Services/CallService.php · PHP
$roomName = 'call-'.Str::uuid()->toString();
$call = VideoCall::query()->create([
'room_name' => $roomName,
'status' => CallStatus::Ringing,
'mode' => $mode, // video | audio
]);
$identity = 'user-'.$actor->id;
$videoGrants = [];
if ($call->mode === CallMode::Audio) {
$videoGrants['canPublishSources'] = [
'microphone', 'screen_share', 'screen_share_audio',
];
}
$token = $this->liveKitTokenService->createToken(
$call->room_name,
$identity,
$actor->name ?? 'Participant',
$videoGrants
);

Signing is ordinary HMAC-SHA256. LiveKit only cares that iss matches the project API key and that the video grant names the room.

php
app/Services/LiveKitTokenService.php · PHP
public function createToken(
string $roomName,
string $identity,
string $displayName,
array $videoGrants = []
): string {
$now = time();
$payload = [
'iss' => config('livekit.api_key'),
'sub' => $identity,
'exp' => $now + (int) config('livekit.token_ttl', 3600),
'name' => $displayName,
'video' => array_merge([
'roomJoin' => true,
'room' => $roomName,
'canPublish' => true,
'canSubscribe' => true,
], $videoGrants),
];
return $this->encodeJwt($payload, config('livekit.api_secret'));
}

5. Connect the Next.js room

video={false} and audio={false} on the room are intentional. Auto-publish on connect fights a warmup path where you already captured camera and mic on the ringing screen. Key the room by call id, not by JWT a token refresh must not remount the room, or remote tiles flicker. Turn adaptive streaming and dynacast on here.

tsx
components/calls/VideoSession.tsx · TSX
const roomKey = callId ?? token.slice(0, 12);
<LiveKitRoom
key={roomKey}
token={token}
serverUrl={serverUrl}
connect
video={false}
audio={false}
connectOptions={{ autoSubscribe: true }}
options={{ adaptiveStream: true, dynacast: true }}
onDisconnected={onDisconnected}
data-lk-theme="default"
>
<PublishLocalTracks mode={callMode} />
<CallConference onEndCall={endCall} />
</LiveKitRoom>

Once signaling is up, publish microphone always, and camera only for a video call:

tsx
await room.localParticipant.setMicrophoneEnabled(true);
if (mode === "video") {
await room.localParticipant.setCameraEnabled(true);
}

Cloud is global, but DNS, TLS, and region selection still take time. While the incoming-call UI is visible we preload the LiveKit bundles, capture camera and mic, and call prepareConnection() so the path to Cloud is warm. Connect the real session with a fresh Room instance reusing the warmup Room caused odd reconnect bugs. Reusing the warmed tracks did not.

6. Guests get the same room, a different identity

A public page collects a display name. The API issues a guest JWT with a guest-{uuid} identity so it never collides with user-{id}. LiveKit does not care which React tree you mount. Authenticated sessions can use a custom conference layout. Guests can use the stock VideoConference component.

php
app/Services/CallService.php · PHP
components/calls/PublishLocalTracks.tsx · TSX
$identity = 'guest-'.Str::uuid();
$token = $this->liveKitTokenService->createToken(
$call->room_name,
$identity,
$guestName,
);
return [
'token' => $token,
'server_url' => config('livekit.url'),
'room_name' => $call->room_name,
'identity' => $identity,
'display_name' => $guestName,
];

7. End the room, and record if you need to

When the session ends, delete the LiveKit room so leftover clients are kicked. Use the PHP SDK as a RoomServiceClient here not as the thing that mints user tokens.

php
app/Services/LiveKitTokenService.php · PHP
$client = new RoomServiceClient(
$this->apiHost(),
config('livekit.api_key'),
config('livekit.api_secret'),
);
$client->deleteRoom($roomName);

Recordings are Cloud room composite egress a grid layout written straight to S3. The Next.js UI only hits your start/stop recording endpoints. It never talks to egress with the API secret.

php
app/Services/LiveKitEgressService.php · PHP
$client = new EgressServiceClient(
$this->apiHost(),
config('livekit.api_key'),
config('livekit.api_secret'),
);
$client->startRoomCompositeEgress(
$roomName,
'grid',
$this->buildFileOutput($filePath, $credentials),
);

What LiveKit Cloud actually costs

Cloud is a monthly plan plus meters. Figures below are from LiveKit’s pricing page as of 2026; they move. Use them for shape, not for a quote.

PlanPriceRough fit
Build$0/moDevelopment, no card required
Shipfrom $50/moProduction, email support
Scalefrom $500/moRegion pinning, HIPAA BAA, higher limits
EnterprisecustomSSO, Slack, volume deals, SLA

LiveKit does not bill "one minute of a meeting." It bills participant-minutes: one person connected for one minute. A one-minute video call with five people is five WebRTC minutes, not one. A 30-minute 1:1 call is about 60 participant-minutes.

SessionPeopleClock timeBilled as
1:1 video or audio call21 minute2 WebRTC minutes
Small group call51 minute5 WebRTC minutes
1:1 meeting230 minutes60 WebRTC minutes

A 1-minute call with 5 participants costs 5 WebRTC minutes. Bandwidth is a separate meter.

Ship includes on the order of 150,000 WebRTC minutes, then about $0.0005 per minute. Scale includes about 1.5 million minutes, then about $0.0004. Bandwidth is extra on the order of $0.10–$0.12 per GB after the included bucket. That bandwidth line is what surprises teams who only model minutes. Adaptive streaming and dynacast exist partly to keep that line in check.

Recording is its own meter. Agents, inference, and telephony are yet more meters. We did not need those to ship calling. LiveKit’s homepage leads with agents. The SFU pricing is lower on the same page.

Self-hosting has no LiveKit invoice. You pay the cloud provider and the people. It wins at huge volume with an SRE team already in place. It loses when the goal is to ship this quarter.

If you are integrating LiveKit next week

  • Treat calling as a product feature, not a pasted Zoom URL. Then pick a platform that can live inside your app.
  • Prefer an open protocol if you might self-host later. That is the LiveKit argument against Daily, Twilio Video, and Zoom’s SDK.
  • Mint tokens on the server. Give Next.js a token and a URL. Let the browser talk to Cloud.
  • Use one room for video calls and audio calls. Change canPublishSources, not the architecture.
  • Turn on adaptive streaming and dynacast. Then model cost as participant-minutes five people for one minute is five minutes plus bandwidth.
  • Delete the room when the session ends. Record to your own bucket if you need a tape.

Closing

Embedded video calling is not a widget you drop in after lunch, and it is not a reason to run your own media cluster. LiveKit Cloud sits in the middle: WebRTC with a managed SFU, a JWT your API can mint, and a React kit you can actually restyle.

Keep the API secret on the server. Give the Next.js app a token and a URL. Let the browser talk to Cloud. Ringing, guests, audio vs video, recording, and who may publish a camera stay in the domain logic you already had to write.

Source: https://livekit.com/pricing · Pricing figures as of 2026 and subject to change. Competitor prices from public pages in the same period.

Blogs

Discover the latest insights and trends in technology with the Omax Tech Blog.

View All Blogs
Omax | Blog | We chose ECS over EKS: what we gained and what we gave up
8-10 min
September 10, 2026

We chose ECS over EKS: what we gained and what we gave up

An honest comparison of ECS vs EKS the costs, tradeoffs, and real-world reasoning behind choosing ECS for a production platform on AWS.

Read More
Omax | Blog | Upgrading Legacy Systems: From Outdated Technology to Competitive Advantage
8-10 min
September 07, 2026

Upgrading Legacy Systems: From Outdated Technology to Competitive Advantage

Learn how to upgrade legacy systems through application modernization, API integration, cloud migration, security improvements, and incremental system upgrades without disrupting business operations.

Read More
Omax | Blog | Building Distributed Tracing and Observability with AWS X-Ray
12-14 min
September 04, 2026

Building Distributed Tracing and Observability with AWS X-Ray

A practical guide to correlating requests across a multi-tier application using correlation IDs, AWS X-Ray segments, and structured logging for faster incident debugging.

Read More
Omax | Blog | Designing Before and After AI: What Really Changed
6-7 min
September 03, 2026

Designing Before and After AI: What Really Changed

A look at how AI has transformed UI/UX design from manual wireframes and slow research to AI-assisted prototyping, design-to-code, and personalization at scale.

Read More
Omax | Blog | Beyond Prompting: Managing Context and Tokens in AI Coding Tools
12-14 min
September 03, 2026

Beyond Prompting: Managing Context and Tokens in AI Coding Tools

Ever wondered why your AI coding agent starts losing context or hits a hard limit mid-task? The answer lies in tokens and the context window. Good AI coding is not about giving the model the most information. It is about giving it the right information at the right time.

Read More
Omax | Blog | What Is llms.txt? How It Helps Google, AI Search, and Agentic Browsing Find Your Website
10-12 min
August 31, 2026

What Is llms.txt? How It Helps Google, AI Search, and Agentic Browsing Find Your Website

Learn what llms.txt is, how it differs from sitemap.xml and robots.txt, and how it can help your site get found by Google, AI search tools, and AI agents.

Read More
Omax | Blog | Build an Automated Image Compression Script with Sharp and SVGO
7-8 min
August 28, 2026

Build an Automated Image Compression Script with Sharp and SVGO

Compress images from the terminal with a Node.js script powered by Sharp and SVGO a safe, two-step workflow that keeps your site fast without bloating your repo.

Read More
Omax | Blog | The Right Way to Migrate from MySQL to AWS Aurora DSQL
7-8 min
August 25, 2026

The Right Way to Migrate from MySQL to AWS Aurora DSQL

Migrating a production database is one of the highest-risk changes you can make to an application. Moving from MySQL to AWS Aurora DSQL raises the stakes further...

Read More
Omax | Blog | From Memory Nightmare to Serverless: Bundling Files into a ZIP with AWS Lambda
8-10 min
August 25, 2026

From Memory Nightmare to Serverless: Bundling Files into a ZIP with AWS Lambda

A straightforward 'download all these files as one ZIP' request that worked perfectly on my laptop and... fell over the first day it met real production load. Here's the debugging story, the scaling options I ruled out, and why AWS Lambda was the right answer.

Read More