# How to Add Video Calls to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Video calling needs a WebRTC SFU service (Daily.co or Livekit Cloud), a Supabase Edge Function that generates meeting tokens, and a video grid UI that handles the NotAllowedError when users deny camera permission. With v0 or Lovable you can ship working 1-to-many calls in 6-12 hours. Daily.co free tier covers 10,000 participant-minutes/mo ($0/mo for small apps); usage-based pricing kicks in at $0.00099/participant-minute after that.

## What Video Calls Actually Require to Build

Video calling is the most technically complex communication feature on this list. You are not just rendering a UI — you are negotiating a live peer-to-peer media connection through firewalls, managing microphone and camera permissions in the browser, and handling the participant grid resizing as users join and leave. Building WebRTC from scratch takes 4-8 weeks. The practical approach is to use a Selective Forwarding Unit (SFU) service — Daily.co or Livekit — that handles ICE negotiation, STUN/TURN servers, and codec selection for you. Your code becomes: generate a room token on the server, load the SDK in the client, and render the video tiles. The product decisions are about the UX around the call — permission handling, the controls bar, screen sharing, and the participant grid layout.

## Anatomy of the Feature

Seven components make up a production video call feature. The SDK handles the hardest parts — your code lives in the room creation API and the video grid UI. Camera permission errors are where first builds get stuck.

- **WebRTC peer connection layer** (backend): Daily.co JavaScript SDK (@daily-co/daily-js) or Livekit Client SDK (livekit-client) handles all ICE negotiation, STUN/TURN server traversal, codec selection, and bitrate adaptation automatically. You call DailyIframe.createCallObject() or new Room() — the SDK takes it from there.
- **Room creation API** (backend): A Supabase Edge Function calls the Daily.co Rooms API (POST /v1/rooms) with the DAILY_API_KEY stored in Supabase Secrets. The API returns a room URL and meeting token that expires in 60 minutes. The Edge Function inserts the room record into the call_rooms Supabase table and returns the token to the client.
- **Video and audio tile grid** (ui): A CSS Grid layout where each tile is a video element fed by the SDK's participant hooks. Daily.co provides useParticipant() React hooks; Livekit provides @livekit/components-react with pre-built VideoTrack and AudioTrack components. The grid uses repeat(auto-fill, minmax(300px, 1fr)) to reflow automatically as participants join.
- **Call controls bar** (ui): A shadcn/ui Button row fixed at the bottom of the call view: mute microphone toggle, camera on/off toggle, share screen button, and end call button. State is managed via Daily's useLocalParticipant() hook or Livekit's useLocalParticipant hook — the SDK is the source of truth for whether the mic is actually muted.
- **Screen sharing** (service): navigator.mediaDevices.getDisplayMedia() triggers the browser's native screen picker. Both Daily.co and Livekit wrap this in their SDK so screen share appears in the participant grid automatically without additional configuration.
- **Call invite and room link** (data): A Supabase call_rooms table stores the Daily.co room URL, the host user ID, creation time, and expiry time. Participants join by receiving the room URL via in-app messaging or email. Any authenticated user who has the room ID can SELECT the room URL via RLS.
- **Permission error handler** (ui): getUserMedia throws NotAllowedError when the user denies camera or microphone permission. This must be caught and handled with an informative modal that shows browser-specific instructions for re-enabling permissions — the generic 'something went wrong' error loses users.

## Data model

One table tracks call rooms — host, status, and expiry. Run this in the Supabase SQL editor.

```sql
create table public.call_rooms (
  id uuid primary key default gen_random_uuid(),
  room_url text not null,
  room_name text not null,
  host_user_id uuid references auth.users(id) on delete cascade not null,
  status text not null default 'waiting' check (status in ('waiting', 'active', 'ended')),
  created_at timestamptz not null default now(),
  expires_at timestamptz not null default (now() + interval '60 minutes')
);

alter table public.call_rooms enable row level security;

-- Authenticated users can create rooms
create policy "host can insert"
  on public.call_rooms for insert
  to authenticated
  with check (auth.uid() = host_user_id);

-- Any authenticated user can read a room if they have the room id
create policy "authenticated can select"
  on public.call_rooms for select
  to authenticated
  using (true);

-- Host can update room status
create policy "host can update status"
  on public.call_rooms for update
  using (auth.uid() = host_user_id);

create index call_rooms_host_idx
  on public.call_rooms (host_user_id, created_at desc);

create index call_rooms_status_idx
  on public.call_rooms (status)
  where status != 'ended';
```

The status index filtered to non-ended rooms keeps the active rooms query fast. The expires_at column lets you build a scheduled Edge Function or pg_cron job that marks rooms as 'ended' when the Daily.co token expires.

## Build paths

### Lovable — fit 3/10, 6-10 hours

Lovable can scaffold the Edge Function for Daily.co room creation and the video call UI using the Daily.co SDK. Camera access is blocked in the Lovable preview — all testing must happen on the published URL.

1. Create a new Lovable project and connect Lovable Cloud so Supabase Edge Functions are available
2. Add DAILY_API_KEY to the Secrets panel (Cloud tab) before running the prompt — the Edge Function needs it at deploy time
3. Paste the prompt below in Agent Mode — Lovable will generate the call room Edge Function, the video grid component, and the call controls
4. Publish to your Lovable URL and open it on a real device — the camera will not activate in the Lovable editor preview under any circumstances
5. Test with two browser tabs open on the published HTTPS URL logged in as different users; verify mute, camera toggle, and end call all update the SDK state correctly

Starter prompt:

```
Build a video call feature using the Daily.co SDK (@daily-co/daily-js). Step 1 — Supabase Edge Function 'create-call-room': call the Daily.co Rooms API (POST https://api.daily.co/v1/rooms) using DAILY_API_KEY from Deno.env.get(). Set meeting token expiry to 60 minutes. Insert a row into call_rooms table (room_url, room_name, host_user_id, status, expires_at). Return the room URL and token to the client. Step 2 — VideoCallPage component: on load, call the create-call-room Edge Function to get a room URL and token. Join the room using DailyIframe.createCallObject(). Render a CSS grid of participant video tiles using useParticipants() hook — each tile is a video element fed by the participant's videoTrack. Use repeat(auto-fill, minmax(300px, 1fr)) grid so the layout reflows as participants join. Camera is OFF by default when joining (startVideoOff: true in join options). Step 3 — Call controls bar (fixed bottom): mute mic button (useLocalParticipant() for state), camera toggle button, share screen button using startScreenShare(), end call button that calls leave() and navigates back to app. Participant count shown in the top right. Step 4 — Permission error handler: catch NotAllowedError from getUserMedia and show a modal with browser-specific instructions to re-enable camera and microphone. Show a 'Camera preview not available in editor' banner in the call UI so users know to test on the published URL. Screen sharing button falls back gracefully if getDisplayMedia is not supported — show a 'Screen sharing not supported in this browser' toast.
```

Limitations:

- Camera and microphone access is blocked in the Lovable preview iframe — all video call testing must happen on the published HTTPS URL
- WebRTC handshake errors are difficult to debug in the Lovable chat loop — use the Daily.co dashboard and browser DevTools Network tab on the published URL
- The Daily.co SDK must be explicitly named in the prompt; without it Lovable may attempt to implement WebRTC from scratch

### V0 — fit 4/10, 5-8 hours

Best path for video calls: Next.js App Router's natural client/server boundary maps cleanly to the Daily.co pattern — server-side room creation in an API route, client-side SDK in a 'use client' component.

1. Prompt v0 with the spec below to generate the VideoCallPage, ParticipantGrid, and CallControls components plus the /api/create-room route
2. Open the Vars panel and add DAILY_API_KEY (server-only) and NEXT_PUBLIC_SUPABASE_URL with the anon key
3. Run the SQL schema from this page in the Supabase SQL editor
4. Deploy to Vercel — camera access requires HTTPS; the v0 sandbox preview cannot access camera or microphone
5. Open the deployed URL on two browser tabs and test the full call flow: join, mute, camera toggle, screen share, and leave

Starter prompt:

```
Build a video call feature for a Next.js 14 App Router app using the Daily.co SDK (@daily-co/daily-js). API route /api/create-room (POST, server-side): call Daily.co Rooms API with DAILY_API_KEY; create a room with exp set to 60 minutes from now; return { roomUrl, token }. VideoCallPage (client component — 'use client'): on mount, fetch /api/create-room to get roomUrl and token; call DailyIframe.createCallObject() and join(roomUrl, { token, startVideoOff: true }). ParticipantGrid (client component): use useParticipants() to get participant list; render CSS grid with repeat(auto-fill, minmax(300px, 1fr)); each tile renders a video element with participant.videoTrack as srcObject; show display name overlay in bottom-left of each tile. CallControls (client component): fixed bar at bottom with four shadcn/ui icon buttons — toggle mic (useLocalParticipant().isMicMuted), toggle camera (useLocalParticipant().isCamMuted), share screen (call.startScreenShare()), end call (call.leave() then router.push('/')). Participant count badge in header. Permission error handler: wrap getUserMedia in try/catch; on NotAllowedError show a shadcn/ui Dialog with instructions for Chrome and Firefox to re-enable permissions. Screen share falls back with a toast if getDisplayMedia is unavailable. Wire track.onended on screen share tracks to reset the share button state when user stops sharing from the browser UI. Include TypeScript types for DailyParticipant.
```

Limitations:

- Supabase is not auto-provisioned — create the call_rooms table manually and add the Daily.co API key to Vercel environment variables
- Camera access is blocked in the v0 sandbox preview — deploy to Vercel to test any video functionality
- The Daily.co free tier covers 10,000 participant-minutes/mo; higher volumes require a paid Daily.co plan

### Custom — fit 2/10, 4-8 weeks

Only justified for HIPAA-compliant telehealth with a Business Associate Agreement, server-side recording pipelines, custom TURN servers for corporate firewalls, or AI-enhanced media pipelines.

1. HIPAA compliance: Livekit self-hosted on a HIPAA-compliant cloud provider with BAA; server-side recording encrypted at rest; audit logs for every session
2. Custom recording pipeline: Livekit Egress service captures room streams to S3-compatible storage; transcription via Whisper API; searchable transcript stored in PostgreSQL
3. Corporate firewall penetration: self-hosted TURN server (coturn) that relays media when peer-to-peer ICE fails through strict NAT
4. AI media pipeline: real-time audio transcription using Deepgram or AssemblyAI streaming API; noise cancellation via Krisp SDK integration

Limitations:

- Custom WebRTC infrastructure is a specialised engineering domain — plan for 4-8 weeks and a dedicated devops engineer for the TURN/media server layer

## Gotchas

- **Camera shows a black screen in Lovable preview** — getUserMedia is blocked by the sandbox attribute on the Lovable preview iframe. The browser's permission prompt never fires and the camera constraint fails silently — the video element renders but stays black. This is not a bug in the generated code; it is a security restriction of the preview environment. Fix: Always test video calls on the published Lovable URL. Add a visible banner in the call UI during development: 'Camera preview not available in the editor — publish to test.' The camera works correctly on the published HTTPS URL on any real device.
- **HTTPS required for screen sharing** — getDisplayMedia() throws NotSupportedError on any non-HTTPS origin, including http://localhost and any iframe without the allow='display-capture' attribute. The screen share button appears to do nothing — no error message, no browser dialog, just silence. Fix: Deploy to a real HTTPS URL before testing screen sharing. Vercel and Lovable's published URLs both use HTTPS by default. For local development, localhost is a browser exception — screen sharing works on localhost but not on a local network IP like 192.168.x.x.
- **Daily.co token fails to generate — 401 from Edge Function** — The Edge Function calls the Daily.co Rooms API but the DAILY_API_KEY is not present in Supabase Secrets. Daily.co returns a 401 Unauthorized response. The Edge Function either returns an error to the client or fails silently, and the video page stays on the loading state indefinitely. Fix: Add DAILY_API_KEY to Supabase Secrets via the Cloud tab before deploying the Edge Function. Verify it is set by checking the Supabase Edge Function logs — the log will show '401' if the key is missing. The key is found in the Daily.co dashboard under Developers.
- **Participant grid breaks at 5 or more users** — AI generates a fixed 2-column CSS grid. When a 5th participant joins, their video tile overflows the viewport and is cut off or wraps to a third row that breaks the layout. On mobile, 2 columns is already too many for small screens. Fix: Replace the fixed grid with: display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)). This reflows dynamically as participants join. Test with at least 4 browser tabs open simultaneously on the published HTTPS URL.
- **Screen share button stays active after user stops sharing** — When a user stops screen sharing from the browser's native UI (the 'Stop sharing' button Chrome shows at the bottom of the screen), the MediaStreamTrack.onended event fires — but the SDK's track.onended event is not wired in the generated code. The in-app share button continues to show the active sharing state. Fix: Subscribe to the track's ended event in the SDK. In Daily.co: listen to the screen-share-stopped event on the call object and reset the share button state. In Livekit: subscribe to TrackEvent.Ended on the local screen share track.

## Best practices

- Name the SDK explicitly in your prompt — Daily.co or Livekit — never let the AI choose generically, or it may attempt native WebRTC implementation
- Set camera OFF by default when joining (startVideoOff: true) — users prefer to control when they appear on camera
- Always read mic and camera state from the SDK hooks, not from local React state — the SDK is the source of truth for what the hardware is actually doing
- Test with 4 browser tabs on the published URL before launch — the 4-participant case almost always reveals grid layout and audio feedback issues
- Wire track.onended on screen share tracks so the in-app button resets when the user stops sharing from the browser UI
- Add a 'Camera not available in editor' banner in development to save debugging time when camera appears black
- Store the Daily.co API key only in Supabase Secrets or Vercel environment variables — never in client-side code or committed to the repository

## Frequently asked questions

### Can I add video calls to a Lovable app?

Yes — Lovable can scaffold the Daily.co Edge Function for room creation and the video grid component in a single prompt. The important limitation is that camera access is blocked in the Lovable editor preview. You must publish the app and test video calls on the published HTTPS URL. Build it in Lovable; test it outside Lovable.

### What is the cheapest way to add video calling to a web app?

Daily.co free tier covers 10,000 participant-minutes per month — roughly 167 one-hour calls. Livekit Cloud free tier covers 50,000 minutes. For a small app, the total cost is $0/mo. You only need to pay when usage consistently exceeds the free tier, at which point Daily.co charges $0.00099/participant-minute (approximately $0.06 per one-hour call).

### Do I need to build WebRTC from scratch?

No — and you should not try. WebRTC ICE negotiation, STUN/TURN server infrastructure, and codec selection are not problems you want to solve while building a product. Daily.co and Livekit handle all of it. Your code is: generate a room token on the server, call the SDK on the client, render the video tiles. The entire integration is 100-200 lines of code.

### Can users share their screen during a video call?

Yes — both Daily.co and Livekit support screen sharing through the standard browser getDisplayMedia() API. The SDK wraps it so the shared screen appears as a participant tile in the grid automatically. Screen sharing requires HTTPS and does not work in the Lovable preview iframe or on http:// URLs.

### Why does the camera show a black screen in the Lovable editor?

The Lovable editor preview runs inside a sandboxed iframe that blocks camera and microphone access for security. This is not a bug in your code. The camera works correctly on the published Lovable URL. Always test video features on the published URL, not in the editor. Add a visible banner in the call UI that says so during development.

### How many participants can join a video call?

Daily.co supports up to 1,000 participants in a single session on paid plans; the free tier is intended for small group calls. Livekit Cloud supports similar scale. For most apps (team calls, tutoring, 1-to-1 consultations) you will never need more than 10-20 participants and the free tier is more than sufficient.

### Does video calling work on mobile browsers?

Yes — WebRTC is supported on iOS Safari 14.1+ and Android Chrome. iOS requires the user to explicitly grant camera and microphone permissions via the browser's prompt. The camera and mic permission UI on iOS is different from desktop — include instructions for iOS in your permission error handler. Screen sharing is not available on iOS Safari as of 2026.

### How do I record video calls?

Daily.co offers a cloud recording API that saves MP4 files to your Daily.co storage or an S3-compatible bucket. Enable it by passing the start_cloud_recording property when creating the room. Livekit has an Egress service for the same purpose. Both require a paid plan. Client-side recording via the MediaRecorder API is possible but produces lower quality and puts the recording burden on the participant's device.

---

Source: https://www.rapidevelopers.com/app-features/video-calls
© RapidDev — https://www.rapidevelopers.com/app-features/video-calls
