Next.js video conferencing app with the Zoom Video SDK
-
Update: February 18, 2026
The blog now uses the Zoom Video SDK for React (@zoom/videosdk-react) for a simpler React integration. -
Update: April 10, 2025 The blog has been updated to support Next.js v15.
The Zoom Video SDK provides video, audio, screen sharing, chat, and more with an easy to use SDK. In this blog we'll showcase adding video chat to a Next.js web application. This blog will cover both generating JSON Web Tokens (JWTs) on the server-side to authenticate a video session and building the video chat interface using the Zoom Video SDK for React.
If you're building a React application without Next.js, you can skip to the Build the Videochat component section and use your own JWT generation logic. A demo built with React & Vite is available on GitHub.
For this blog, we’ll use the app router. You can find the code on GitHub. A demo with the pages router is also available on the pages-router branch.
Prerequisites:
- Node LTS & NPM
- A Zoom Video SDK Account
Step 1: Scaffold the application
If you already have a Next.js project, you can skip this step. To scaffold our application we'll be using Next.js CLI.
Open a terminal and execute:
npx create-next-app@latest
Go ahead and select the config you like, we chose most of the defaults:
What is your project named? videosdk-nextjs-quickstart
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No
Step 2: Configuring the project
Install the dependencies
We will install the Zoom Video SDK for Web (@zoom/videosdk), the Zoom Video SDK for React (@zoom/videosdk-react), jsrsasign to generate JWTs, and server-only to keep server-only code out of the client bundle. You can use the following command for installation:
npm install @zoom/videosdk @zoom/videosdk-react jsrsasign server-only
Script to enable shared array buffers
To leverage the full power of the Zoom Video SDK—including features like rendering multiple videos, virtual background, and background noise suppression—we need to enable support for Shared Array Buffers (SAB).
Simply download this file and place it in the public folder of your project as public/coi-serviceworker.js. Later, we’ll import this file to enable SAB support. You can read more about SAB in our documentation.
Add Environment Variables
To complete the setup, create a .env file in the root of our project and add the Zoom Video SDK Key and Secret to it. You can find your SDK Key and Secret in the Video SDK Dashboard, by clicking on the Develop button and selecting Build Video SDK. Make sure you're logged in to your Video SDK account.
ZOOM_SDK_KEY="Your Zoom SDK Key"
ZOOM_SDK_SECRET="Your Zoom SDK Secret"
Step 3: Authenticate a session
The Zoom Video SDK uses JWTs to authenticate a session. We can define a function getData in /src/data/getToken.ts to generate a JWT based on the session slug. Creating a JWT requires the SDK Key and Secret. We'll import server-only to make sure this function will only be executed on the server, keeping our SDK key and secret secure. This will create a JWT using the generateSignature function and return it.
import "server-only";
export async function getData(slug: string) {
const JWT = await generateSignature(slug, 1);
return JWT;
}
The generateSignature function will use the jsrsasign library and the session name to generate the JWT:
import { KJUR } from "jsrsasign";
function generateSignature(sessionName: string, role: number) {
if (!process.env.ZOOM_SDK_KEY || !process.env.ZOOM_SDK_SECRET) {
throw new Error("Missing ZOOM_SDK_KEY or ZOOM_SDK_SECRET");
}
const iat = Math.round(new Date().getTime() / 1000) - 30;
const exp = iat + 60 * 60 * 2;
const oHeader = { alg: "HS256", typ: "JWT" };
const sdkKey = process.env.ZOOM_SDK_KEY;
const sdkSecret = process.env.ZOOM_SDK_SECRET;
const oPayload = {
app_key: sdkKey,
tpc: sessionName,
role_type: role,
version: 1,
iat: iat,
exp: exp,
};
const sHeader = JSON.stringify(oHeader);
const sPayload = JSON.stringify(oPayload);
const sdkJWT = KJUR.jws.JWS.sign("HS256", sHeader, sPayload, sdkSecret);
return sdkJWT;
}
Step 4: Dynamic route for video chat
We'll create a Server Component at the /call/<slug> dynamic route. We can use this to create a link for each session based on user input. Users on the same link will be able to join the same session.
We can import and call the getData function to obtain a JWT and use it within the Page component in /src/call/[slug]/page.tsx.
import { getData } from "@/data/getToken";
export default async function Page({ params }: { params: { slug: string } }) {
const jwt = await getData(params.slug);
return (
<main>
</main>
);
}
We add a Script tag from next/script and import the coi-serviceworker.js file we added earlier. We'll also add a client component VideochatClientWrapper that will dynamically load and render the video chat UI (which uses @zoom/videosdk-react).
import { getData } from "@/data/getToken";
import VideochatClientWrapper from "@/components/VideochatClientWrapper";
import Script from "next/script";
export default async function Page(props: { params: Promise<{ slug: string }> }) {
const params = await props.params;
const jwt = await getData(params.slug);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<VideochatClientWrapper slug={params.slug} JWT={jwt} />
<Script src="/coi-serviceworker.js" strategy="beforeInteractive" />
</main>
);
}
Let's define the VideochatClientWrapper component in src/components/VideochatClientWrapper.tsx. We'll add the use client directive at the top of the file to mark it as a client component. We'll use next/dynamic to load the video chat component on the client side and disable server-side rendering. The loaded component uses @zoom/videosdk-react, which needs browser APIs. Loading it with next/dynamic and ssr: false ensures it only runs on the client.
'use client';
import dynamic from "next/dynamic";
const Videochat = dynamic<{ slug: string; JWT: string }>(() => import("./Videochat"), { ssr: false });
export default function VideochatClientWrapper({ slug, JWT }: { slug: string; JWT: string }) {
return <Videochat slug={slug} JWT={JWT} />;
}
Step 5: Build the Videochat component
The video chat UI lives in src/components/Videochat.tsx. We use the "use client" directive so it runs on the client. We'll create a Container component that displays a button to "Join session". When the user clicks it, we'll render the Videochat component that will render the video chat UI. We'll define this next.
"use client";
import { CSSProperties, type SetStateAction, type Dispatch, useState } from "react";
import { Mic, MicOff, Video, VideoOff } from "lucide-react";
import { useVideoState, useAudioState } from "@zoom/videosdk-react";
import { PhoneOff } from "lucide-react";
import { Button } from "./ui/button";
import { useSession, useSessionUsers, VideoPlayerComponent, VideoPlayerContainerComponent } from "@zoom/videosdk-react";
const userName = `User-${new Date().getTime().toString().slice(8)}`;
const Container = (props: { slug: string; JWT: string }) => {
const [inCall, setInCall] = useState(false);
return inCall ? (
<Videochat {...props} setInCall={setInCall} />
) : (
);
};
Videochat UI
The Videochat component takes slug, JWT, and setInCall. It uses:
useSession(session, JWT, userName)— joins the session and exposes the loading and error statesuseSessionUsers()— list of participants to render
We handle loading and error, then render the session title and the video grid
const Videochat = (props: { slug: string; JWT: string; setInCall: Dispatch<SetStateAction<boolean>> }) => {
const { slug: session, JWT, setInCall } = props;
const { isLoading, isError, isInSession, error } = useSession(session, JWT, userName);
const participants = useSessionUsers();
const { isVideoOn, toggleVideo } = useVideoState();
const { isAudioMuted, toggleMute } = useAudioState();
if (isLoading) return Loading...;
if (isError) return Error: {error?.reason};
return (
<h1 className="text-center text-3xl font-bold mb-4 mt-0">
Session: {session}
</h1>
<div>
{isInSession && (
<VideoPlayerContainerComponent style={videoPlayerStyle}>
{participants.map(participant => (
<VideoPlayerComponent
key={participant.userId}
user={participant}
/>
))}
</VideoPlayerContainerComponent>
)}
We use the VideoPlayerContainerComponent to render the video grid and the VideoPlayerComponent to render the individual participant videos by passing in the user object. These components automatically attach and detach the video streams. When a new user joins or updates their video state, the components re-render based on the participant state.
To display controls for the camera and microphone we can use:
useVideoState()—isVideoOnandtoggleVideofor the camera button.useAudioState()—isAudioMutedandtoggleMutefor the microphone button.
<div className="mt-4 flex w-[30rem] flex-1 justify-around self-center rounded-md bg-white p-4">
</div>
</div>
);
};
export default Container;
Add the videoPlayerStyle object:
const videoPlayerStyle = {
height: "75vh",
marginTop: "1.5rem",
marginLeft: "3rem",
marginRight: "3rem",
alignContent: "center",
borderRadius: "10px",
overflow: "hidden",
} as CSSProperties;
That's all the code needed to build a video conferencing app using the Zoom Video SDK for React. Run npm run dev in the project root; the app will be available at http://localhost:3000/.
Conclusion
We hope this quick guide has given you a good starting point to build your own video conferencing app using the Zoom Video SDK with React and Next.js. We've covered the basics of setting up the project, configuring the app, and building the video conferencing features.
We have guides for the same app built with Vue & Nuxt, SolidStart and SvelteKit, be sure to check those out as well!