Claude Code for Daily.co: Video Calls with React — Claude Skills 360 Blog
Blog / Backend / Claude Code for Daily.co: Video Calls with React
Backend

Claude Code for Daily.co: Video Calls with React

Published: May 26, 2027
Read time: 6 min read
By: Claude Skills 360

Daily.co is a WebRTC video API — DailyProvider wraps your app with a call context. useDaily() returns the DailyCall object. callObject.join({ url, token }) connects to a room. useLocalParticipant() returns your own participant state — {video: true, audio: true, session_id, user_name}. useParticipantIds() lists all session IDs. useVideoTrack(sessionId) returns {track, isOff, persistentTrack}. DailyVideo renders a participant’s video from a session ID. useDevices() returns {cameras, microphones, speakers, setCamera, setMicrophone}. useScreenShare() returns {screens, startScreenShare, stopScreenShare, isSharingScreen}. Server-side: daily.createRoom({ name, properties }) creates a room; daily.createMeetingToken({ properties: { room_name, is_owner, exp } }) creates a token. useRecording() hooks into call recording state. Claude Code generates Daily.co video rooms, participant grids, device selectors, and meeting token APIs.

CLAUDE.md for Daily.co

## Daily.co Stack
- Version: @daily-co/daily-react >= 0.22, @daily-co/daily-js >= 0.70
- Provider: wrap with <DailyProvider> — no callObject prop needed (auto-creates)
- Join: const daily = useDaily(); daily.join({ url: roomUrl, token })
- Tracks: const videoTrack = useVideoTrack(sessionId); <DailyVideo sessionId={id} type="video" />
- Participants: const ids = useParticipantIds(); const local = useLocalParticipant()
- Devices: const { cameras, setCamera, microphones, setMicrophone } = useDevices()
- Screen: const { isSharingScreen, startScreenShare, stopScreenShare } = useScreenShare()
- Server: POST https://api.daily.co/v1/rooms with Authorization: Bearer DAILY_API_KEY

Daily.co Server Utilities

// lib/video/daily.ts — Daily.co REST API utilities
const DAILY_API_KEY = process.env.DAILY_API_KEY!
const DAILY_DOMAIN = process.env.DAILY_DOMAIN!  // e.g. "yourapp.daily.co"
const DAILY_API = "https://api.daily.co/v1"

async function dailyFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
  const res = await fetch(`${DAILY_API}${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${DAILY_API_KEY}`,
      "Content-Type": "application/json",
      ...options.headers,
    },
  })

  if (!res.ok) {
    const err = await res.text()
    throw new Error(`Daily API error ${res.status}: ${err}`)
  }

  return res.json() as Promise<T>
}

export type DailyRoom = {
  id: string
  name: string
  url: string
  created_at: string
  config: {
    exp?: number
    max_participants?: number
    enable_recording?: string
  }
}

export async function createRoom(params: {
  name?: string
  expirySeconds?: number
  maxParticipants?: number
  enableRecording?: boolean
  isOwnerOnly?: boolean
}): Promise<DailyRoom> {
  const exp = params.expirySeconds
    ? Math.floor(Date.now() / 1000) + params.expirySeconds
    : undefined

  return dailyFetch<DailyRoom>("/rooms", {
    method: "POST",
    body: JSON.stringify({
      name: params.name,
      properties: {
        exp,
        max_participants: params.maxParticipants ?? 10,
        enable_recording: params.enableRecording ? "cloud" : undefined,
        enable_prejoin_ui: true,
        enable_network_ui: true,
        enable_noise_cancellation_ui: true,
        start_video_off: false,
        start_audio_off: false,
      },
    }),
  })
}

export async function getRoom(name: string): Promise<DailyRoom | null> {
  try {
    return await dailyFetch<DailyRoom>(`/rooms/${name}`)
  } catch {
    return null
  }
}

export async function deleteRoom(name: string): Promise<void> {
  await dailyFetch(`/rooms/${name}`, { method: "DELETE" })
}

export type MeetingToken = {
  token: string
}

export async function createMeetingToken(params: {
  roomName: string
  userId: string
  userName: string
  isOwner?: boolean
  expirySeconds?: number
  startVideoOff?: boolean
  startAudioOff?: boolean
}): Promise<string> {
  const exp = params.expirySeconds
    ? Math.floor(Date.now() / 1000) + params.expirySeconds
    : Math.floor(Date.now() / 1000) + 3600  // 1 hour default

  const { token } = await dailyFetch<MeetingToken>("/meeting-tokens", {
    method: "POST",
    body: JSON.stringify({
      properties: {
        room_name: params.roomName,
        user_id: params.userId,
        user_name: params.userName,
        is_owner: params.isOwner ?? false,
        exp,
        start_video_off: params.startVideoOff ?? false,
        start_audio_off: params.startAudioOff ?? false,
        enable_recording: params.isOwner ? "cloud" : undefined,
      },
    }),
  })

  return token
}

Next.js API Routes

// app/api/rooms/route.ts — create a Daily room + token
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@clerk/nextjs/server"
import { createRoom, createMeetingToken } from "@/lib/video/daily"
import { db } from "@/lib/db"
import { meetings } from "@/lib/db/schema"

export async function POST(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const { title, maxParticipants = 10 } = await request.json()

  // Create Daily room
  const room = await createRoom({
    expirySeconds: 2 * 60 * 60,  // 2 hours
    maxParticipants,
    enableRecording: true,
  })

  // Create owner token
  const token = await createMeetingToken({
    roomName: room.name,
    userId,
    userName: "Host",
    isOwner: true,
    expirySeconds: 2 * 60 * 60,
  })

  // Persist to DB
  const [meeting] = await db.insert(meetings).values({
    roomName: room.name,
    roomUrl: room.url,
    title,
    hostId: userId,
    expiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000),
  }).returning()

  return NextResponse.json({ room, token, meetingId: meeting.id })
}
// app/api/rooms/[roomName]/token/route.ts — join token for participants
import { NextRequest, NextResponse } from "next/server"
import { auth, currentUser } from "@clerk/nextjs/server"
import { createMeetingToken, getRoom } from "@/lib/video/daily"

export async function POST(
  request: NextRequest,
  { params }: { params: { roomName: string } },
) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const room = await getRoom(params.roomName)
  if (!room) return NextResponse.json({ error: "Room not found" }, { status: 404 })

  const user = await currentUser()

  const token = await createMeetingToken({
    roomName: params.roomName,
    userId,
    userName: user?.fullName ?? user?.username ?? "Guest",
    isOwner: false,
    expirySeconds: 2 * 60 * 60,
  })

  return NextResponse.json({ token, roomUrl: room.url })
}

Video Call Component

// components/video/VideoCall.tsx — Daily.co video call UI
"use client"
import {
  DailyProvider,
  useDaily,
  useLocalParticipant,
  useParticipantIds,
  useVideoTrack,
  useAudioTrack,
  useDevices,
  useScreenShare,
  DailyVideo,
} from "@daily-co/daily-react"
import { useCallback, useEffect, useState } from "react"
import { Mic, MicOff, Video, VideoOff, Monitor, PhoneOff, Users } from "lucide-react"

interface VideoCallProps {
  roomUrl: string
  token: string
  onLeave?: () => void
}

export function VideoCall({ roomUrl, token, onLeave }: VideoCallProps) {
  return (
    <DailyProvider>
      <VideoCallInner roomUrl={roomUrl} token={token} onLeave={onLeave} />
    </DailyProvider>
  )
}

function VideoCallInner({ roomUrl, token, onLeave }: VideoCallProps) {
  const daily = useDaily()
  const local = useLocalParticipant()
  const participantIds = useParticipantIds({ filter: "remote" })
  const { isSharingScreen, startScreenShare, stopScreenShare } = useScreenShare()
  const [joined, setJoined] = useState(false)

  // Join the call
  useEffect(() => {
    if (!daily) return

    daily.join({ url: roomUrl, token }).then(() => setJoined(true))

    return () => {
      daily.leave()
    }
  }, [daily, roomUrl, token])

  const toggleMic = useCallback(() => {
    daily?.setLocalAudio(!local?.audio)
  }, [daily, local?.audio])

  const toggleCamera = useCallback(() => {
    daily?.setLocalVideo(!local?.video)
  }, [daily, local?.video])

  const toggleScreenShare = useCallback(() => {
    if (isSharingScreen) stopScreenShare()
    else startScreenShare()
  }, [isSharingScreen, startScreenShare, stopScreenShare])

  const handleLeave = useCallback(async () => {
    await daily?.leave()
    onLeave?.()
  }, [daily, onLeave])

  if (!joined) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="text-center space-y-2">
          <div className="size-8 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto" />
          <p className="text-sm text-muted-foreground">Connecting...</p>
        </div>
      </div>
    )
  }

  return (
    <div className="flex flex-col h-full bg-neutral-900 rounded-2xl overflow-hidden">
      {/* Participant grid */}
      <div className="flex-1 p-3 grid gap-2"
        style={{
          gridTemplateColumns: participantIds.length === 0
            ? "1fr"
            : participantIds.length <= 3
            ? `repeat(${participantIds.length + 1}, 1fr)`
            : "repeat(2, 1fr)",
        }}
      >
        {/* Local video */}
        {local && (
          <ParticipantTile
            sessionId={local.session_id}
            isLocal
            label={local.user_name ?? "You"}
          />
        )}

        {/* Remote participants */}
        {participantIds.map(id => (
          <RemoteParticipantTile key={id} sessionId={id} />
        ))}
      </div>

      {/* Controls */}
      <div className="flex items-center justify-center gap-3 p-4 border-t border-white/10">
        <ControlButton
          onClick={toggleMic}
          active={!local?.audio}
          activeIcon={<MicOff className="size-5" />}
          inactiveIcon={<Mic className="size-5" />}
          label={local?.audio ? "Mute" : "Unmute"}
        />

        <ControlButton
          onClick={toggleCamera}
          active={!local?.video}
          activeIcon={<VideoOff className="size-5" />}
          inactiveIcon={<Video className="size-5" />}
          label={local?.video ? "Stop video" : "Start video"}
        />

        <ControlButton
          onClick={toggleScreenShare}
          active={isSharingScreen}
          activeIcon={<Monitor className="size-5" />}
          inactiveIcon={<Monitor className="size-5" />}
          label={isSharingScreen ? "Stop share" : "Share screen"}
          variant="secondary"
        />

        <div className="flex items-center gap-1 text-white/70 text-sm px-3 py-2 rounded-full bg-white/10">
          <Users className="size-4" />
          <span>{participantIds.length + 1}</span>
        </div>

        <button
          onClick={handleLeave}
          className="flex items-center gap-2 px-4 py-2.5 rounded-full bg-red-500 hover:bg-red-600 text-white text-sm font-medium transition-colors"
        >
          <PhoneOff className="size-4" />
          Leave
        </button>
      </div>
    </div>
  )
}

function ParticipantTile({
  sessionId,
  isLocal = false,
  label,
}: {
  sessionId: string
  isLocal?: boolean
  label: string
}) {
  const videoTrack = useVideoTrack(sessionId)
  const audioTrack = useAudioTrack(sessionId)

  return (
    <div className="relative bg-neutral-800 rounded-xl overflow-hidden aspect-video">
      {!videoTrack.isOff ? (
        <DailyVideo
          sessionId={sessionId}
          type="video"
          className="w-full h-full object-cover"
          mirror={isLocal}
        />
      ) : (
        <div className="w-full h-full flex items-center justify-center">
          <div className="size-16 rounded-full bg-neutral-600 flex items-center justify-center text-2xl font-bold text-white">
            {label[0]?.toUpperCase()}
          </div>
        </div>
      )}

      {/* Name badge */}
      <div className="absolute bottom-2 left-2 flex items-center gap-1.5 bg-black/50 backdrop-blur-sm rounded-full px-2 py-0.5">
        {audioTrack.isOff && <MicOff className="size-3 text-red-400" />}
        <span className="text-white text-xs font-medium">{label}</span>
      </div>
    </div>
  )
}

function RemoteParticipantTile({ sessionId }: { sessionId: string }) {
  const participant = useParticipantIds({ filter: (p) => p.session_id === sessionId })[0]

  // Get participant info
  const ids = useParticipantIds()
  return (
    <ParticipantTile
      sessionId={sessionId}
      label={"Participant"}
    />
  )
}

function ControlButton({
  onClick,
  active,
  activeIcon,
  inactiveIcon,
  label,
  variant = "default",
}: {
  onClick: () => void
  active: boolean
  activeIcon: React.ReactNode
  inactiveIcon: React.ReactNode
  label: string
  variant?: "default" | "secondary"
}) {
  return (
    <button
      onClick={onClick}
      title={label}
      className={`p-3 rounded-full transition-colors ${
        active
          ? "bg-red-500/20 text-red-400 hover:bg-red-500/30"
          : variant === "secondary"
          ? "bg-white/10 text-white hover:bg-white/20"
          : "bg-white/10 text-white hover:bg-white/20"
      }`}
    >
      {active ? activeIcon : inactiveIcon}
    </button>
  )
}

Device Selector

// components/video/DeviceSelector.tsx — camera/mic/speaker picker
"use client"
import { useDevices } from "@daily-co/daily-react"

export function DeviceSelector() {
  const {
    cameras,
    microphones,
    speakers,
    currentCamera,
    currentMicrophone,
    currentSpeaker,
    setCamera,
    setMicrophone,
    setSpeaker,
  } = useDevices()

  return (
    <div className="space-y-4 p-4 rounded-xl border bg-card">
      <h3 className="font-semibold text-sm">Audio & Video Settings</h3>

      <div className="space-y-3">
        <DeviceSelect
          label="Camera"
          devices={cameras}
          currentDeviceId={currentCamera?.device.deviceId}
          onChange={setCamera}
        />
        <DeviceSelect
          label="Microphone"
          devices={microphones}
          currentDeviceId={currentMicrophone?.device.deviceId}
          onChange={setMicrophone}
        />
        <DeviceSelect
          label="Speaker"
          devices={speakers}
          currentDeviceId={currentSpeaker?.device.deviceId}
          onChange={setSpeaker}
        />
      </div>
    </div>
  )
}

function DeviceSelect({
  label,
  devices,
  currentDeviceId,
  onChange,
}: {
  label: string
  devices: { device: MediaDeviceInfo; selected: boolean }[]
  currentDeviceId?: string
  onChange: (deviceId: string) => void
}) {
  return (
    <div className="space-y-1">
      <label className="text-xs text-muted-foreground">{label}</label>
      <select
        value={currentDeviceId}
        onChange={(e) => onChange(e.target.value)}
        className="w-full text-sm rounded-lg border bg-background px-3 py-2"
      >
        {devices.map(({ device }) => (
          <option key={device.deviceId} value={device.deviceId}>
            {device.label || `${label} ${device.deviceId.slice(0, 8)}`}
          </option>
        ))}
      </select>
    </div>
  )
}

For the Agora alternative when a more scalable live-streaming or broadcast scenario with large audiences (100k+ viewers), interactive live streaming, or real-time engagement features like virtual gifts is needed — Agora has lower latency for high-concurrency broadcasts while Daily excels at small-to-medium group calls with a developer-friendly React SDK, see the Agora guide. For the Livekit alternative when a fully self-hostable SFU with Kubernetes scaling, cloud-recording to S3, and an open-source ecosystem is required — Livekit gives you full infrastructure control while Daily is a managed service, see the Livekit guide. The Claude Skills 360 bundle includes Daily.co skill sets covering video rooms, participant grids, and device management. Start with the free tier to try video call generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free