Feature spec
AdvancedCategory
vertical-tools
Build with AI
6–12 hours with Lovable or V0 (web); 8–14 hours with FlutterFlow (mobile)
Custom build
3–5 weeks custom dev
Running cost
$10–25/mo for small workshops; scales to $500–1,500/mo at 10K users
Works on
Everything it takes to ship Virtual Workshops — parts, prompts, and real costs.
A virtual workshop feature needs five pieces working together: Daily.co for live WebRTC video, Stripe to gate room access behind payment, Supabase Realtime for live chat and polls, a Supabase Edge Function to generate Daily room tokens server-side, and cloud recording for post-workshop replay. Expect 6–12 hours with Lovable or V0. Running costs start at $10–25/month for small workshops and scale to $500–1,500/month at 10,000 users as Daily.co participant-minutes dominate.
What a Virtual Workshop Feature Actually Is
A virtual workshop feature is not a Zoom replacement embedded in your app — it is a structured, gated live experience where attendees pay, join a video room, interact through chat and polls, and leave with a recording. The video layer (Daily.co or LiveKit) is the commodity part; the product decisions are the payment gate that prevents unauthorized access, the host controls that let you run a real session, and the post-workshop replay that extends the value of each event. Builders consistently underestimate how tightly these pieces must be coupled: a Stripe webhook delay that lets users join before payment clears, or a room token generated client-side that exposes your Daily API key, are both critical failures that ship on first attempts.
What users consider table stakes in 2026
- Live video and audio with screenshare accessible in one click after payment — no plugin download required
- Interactive chat and Q&A panel with audience upvoting so the best questions surface to the host
- Real-time polls with visible result bars that update as attendees respond during the session
- Host controls: mute all, spotlight a single speaker, remove a disruptive participant
- Cloud recording starts automatically; attendees receive a playback link by email within minutes of session end
- Capacity enforcement — room should stop accepting joins when max_attendees is reached
Anatomy of the Feature
Six components — the video layer is solved infrastructure; the payment gate and token generation are where first builds fail.
Video conferencing layer
ServiceDaily.co Prebuilt UI or the daily-js React SDK handles WebRTC video, audio, screenshare, and bandwidth adaptation. Daily manages TURN/STUN servers so you never configure WebRTC infrastructure. The Prebuilt UI option embeds a complete call UI in one React component; the custom SDK lets you design the host dashboard from scratch.
Note: Never call the Daily REST API from the browser. Room tokens must be generated in a Supabase Edge Function — your DAILY_API_KEY must never reach the client.
Host control dashboard
UIA React component using react-use-daily hooks reads participant state (camera on/off, speaking, raised hand) and exposes host-only controls: mute all, spotlight speaker, lock room to new joins, remove participant. Role-based rendering shows this panel only when user.isOwner === true from the Daily participant object.
Note: Spotlight mode (showing one participant full-screen for all) is a Daily built-in — call daily.updateParticipant(id, { setSubscribedTracks: { screenVideo: true } }).
Chat and Q&A system
BackendSupabase Realtime channel broadcasts chat messages to all registered attendees. A separate workshop_polls table uses Realtime postgres_changes to push live vote counts. Q&A entries in a workshop_chat table carry an is_question bool and upvote_count int — upvote via Supabase RPC to avoid race conditions on the count column.
Note: One Realtime channel per workshop_id handles both chat and poll subscriptions — do not subscribe per row or you will hit channel limits.
Registration and payment gate
ServiceStripe Checkout creates a payment session for the workshop price. On checkout.session.completed webhook, the Supabase Edge Function marks the registration payment_status as 'confirmed'. A separate token-generation Edge Function checks registration status before issuing a Daily room token — attendees with unconfirmed payment receive a 403.
Note: Wire the Daily token endpoint after the Stripe webhook, not after the Stripe redirect. Redirect speed varies; webhook is authoritative.
Recording and playback
ServiceDaily built-in cloud recording stores session video to Daily's S3. The recording_ready webhook posts the recording URL to your Supabase Edge Function, which saves it to the workshops table and triggers a Resend email to all confirmed attendees. React Player or Video.js renders the recording on the post-workshop page.
Note: Daily cloud recording is billed separately (check Daily pricing). For cost-sensitive builds, use Daily local recording export and self-host on Cloudflare R2.
Workshop data model
DataSupabase tables: workshops (host, schedule, price, capacity, Daily room name), registrations (payment status, join time), workshop_chat (messages and Q&A), workshop_polls (question + jsonb options and responses). RLS restricts chat and polls to confirmed registered attendees only.
Note: Store daily_room_name at workshop creation time — create the Daily room via API at registration open, not at join time, to avoid cold-start delays.
The data model
Run this in the Supabase SQL editor. It creates all four tables with RLS — attendees only see chat and polls for workshops they are registered and paid for.
1create table public.workshops (2 id uuid primary key default gen_random_uuid(),3 host_id uuid references auth.users(id) on delete cascade not null,4 title text not null,5 description text,6 scheduled_at timestamptz not null,7 duration_min int not null default 60,8 price_cents int not null default 0,9 max_attendees int not null default 50,10 daily_room_name text,11 stripe_product_id text,12 recording_url text,13 status text not null default 'scheduled' check (status in ('scheduled','live','ended','cancelled')),14 created_at timestamptz not null default now()15);1617create table public.registrations (18 id uuid primary key default gen_random_uuid(),19 workshop_id uuid references public.workshops(id) on delete cascade not null,20 user_id uuid references auth.users(id) on delete cascade not null,21 stripe_payment_intent_id text,22 payment_status text not null default 'pending' check (payment_status in ('pending','confirmed','refunded')),23 joined_at timestamptz,24 created_at timestamptz not null default now(),25 unique(workshop_id, user_id)26);2728create table public.workshop_chat (29 id uuid primary key default gen_random_uuid(),30 workshop_id uuid references public.workshops(id) on delete cascade not null,31 user_id uuid references auth.users(id) on delete cascade not null,32 message text not null,33 is_question bool not null default false,34 upvote_count int not null default 0,35 created_at timestamptz not null default now()36);3738create table public.workshop_polls (39 id uuid primary key default gen_random_uuid(),40 workshop_id uuid references public.workshops(id) on delete cascade not null,41 question text not null,42 options jsonb not null,43 responses jsonb not null default '{}',44 is_active bool not null default false,45 created_at timestamptz not null default now()46);4748alter table public.workshops enable row level security;49alter table public.registrations enable row level security;50alter table public.workshop_chat enable row level security;51alter table public.workshop_polls enable row level security;5253-- Workshops: hosts see all their own; attendees see scheduled/live/ended54create policy "Host manages own workshops"55 on public.workshops for all56 using (auth.uid() = host_id);5758create policy "Anyone can view public workshops"59 on public.workshops for select60 using (status in ('scheduled', 'live', 'ended'));6162-- Registrations: each user sees their own; host sees registrations for their workshops63create policy "User sees own registrations"64 on public.registrations for select65 using (auth.uid() = user_id);6667create policy "Host sees workshop registrations"68 on public.registrations for select69 using (70 auth.uid() = (select host_id from public.workshops where id = workshop_id)71 );7273create policy "User can register"74 on public.registrations for insert75 with check (auth.uid() = user_id);7677-- Chat: only confirmed attendees or host can read/write78create policy "Confirmed attendees and host can chat"79 on public.workshop_chat for select80 using (81 auth.uid() in (82 select user_id from public.registrations83 where workshop_id = workshop_chat.workshop_id and payment_status = 'confirmed'84 )85 or auth.uid() = (select host_id from public.workshops where id = workshop_chat.workshop_id)86 );8788create policy "Confirmed attendees and host can send messages"89 on public.workshop_chat for insert90 with check (91 auth.uid() in (92 select user_id from public.registrations93 where workshop_id = workshop_chat.workshop_id and payment_status = 'confirmed'94 )95 or auth.uid() = (select host_id from public.workshops where id = workshop_chat.workshop_id)96 );9798-- Polls: visible to confirmed attendees and host99create policy "Confirmed attendees and host can view polls"100 on public.workshop_polls for select101 using (102 auth.uid() in (103 select user_id from public.registrations104 where workshop_id = workshop_polls.workshop_id and payment_status = 'confirmed'105 )106 or auth.uid() = (select host_id from public.workshops where id = workshop_polls.workshop_id)107 );108109create index registrations_workshop_status_idx110 on public.registrations (workshop_id, payment_status);111112create index chat_workshop_created_idx113 on public.workshop_chat (workshop_id, created_at);Heads up: The registrations_workshop_status_idx speeds up the RLS sub-select that checks confirmed payment on every chat read — critical once workshops have 50+ attendees sending rapid messages.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Custom development is the right call when workshops are your core product — breakout rooms, whiteboard collaboration, 50+ attendee SFU architecture, and LMS integration all require full control over the video layer.
Step by step
- 1Evaluate SFU: Daily.co for fastest time-to-production; LiveKit self-hosted on Fly.io or Cloudflare Workers for cost optimization at scale; Agora for lowest latency in Asia-Pacific markets
- 2Build the token service as a standalone Next.js API layer with Redis caching of room states to avoid redundant Daily API calls
- 3Implement breakout room orchestration by creating Daily sub-rooms and issuing scoped tokens that restrict participants to their assigned room
- 4Add LMS integration (Teachable, Thinkific) via their REST APIs to sync workshop completions and issue certificates
- 5Load test with k6 simulating 100 concurrent attendees joining simultaneously to validate token endpoint and Supabase Realtime under real conditions
Where this path bites
- LiveKit self-hosted requires Kubernetes or Fly.io operations expertise — adds 1–2 weeks vs using Daily.co managed infrastructure
- Breakout rooms double your Daily.co participant-minute costs — model this carefully before choosing self-hosted
Third-party services you'll need
Virtual workshops require four external services working together. Daily.co participant-minutes are the variable cost that dominates at scale.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Daily.co | WebRTC video, audio, screenshare, TURN servers, and cloud recording infrastructure | 10,000 participant-minutes/month free | $0.00099/participant-minute after free tier (approx); cloud recording billed additionally |
| Stripe | Workshop registration payment; deposit or full payment; refund handling on cancellation | No monthly fee | 2.9% + 30¢ per transaction |
| Supabase | Database, Auth, Realtime for chat and polls, Edge Functions for token generation and webhooks | 2 projects, 500MB DB, 2M Realtime messages | Pro $25/mo |
| Resend | Registration confirmation email, recording-ready notification, cancellation and refund emails | 3,000 emails/month | $20/mo for 50,000 emails (approx) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Daily.co free tier (10K minutes) covers ~3 workshops of 60 min with 30 attendees each. Supabase free tier. Stripe per-transaction fees on ticket sales. Resend free tier.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Daily.co getUserMedia blocked in preview iframe
Symptom: The Lovable preview iframe and V0 sandbox both run in a sandboxed iframe that does not set allow='camera; microphone' — WebRTC calls to getUserMedia throw NotAllowedError silently, and the video element stays black. Developers assume the camera integration is broken when the app is actually fine.
Fix: Test all video features exclusively on the published HTTPS URL. In Lovable, scan the mobile preview QR code or click Publish. In V0, deploy to production first. Add a visible 'open in full window' button on the room page as a permanent escape hatch.
Daily room token generated client-side exposes API key
Symptom: AI tools frequently generate the Daily room token by calling the Daily REST API directly from a React component using fetch(). This exposes DAILY_API_KEY in the browser's network tab and allows anyone to create unlimited Daily rooms on your account.
Fix: Generate all Daily tokens in a Supabase Edge Function that reads DAILY_API_KEY from Deno.env.get(). The client sends only the workshop_id and its Supabase auth token. The Edge Function verifies payment_status before calling Daily and never returns the raw API key.
Stripe webhook fires after user already tried to join
Symptom: Stripe webhook delivery averages 1–3 seconds after payment completion. If you redirect users to the room immediately on Stripe's success_url, they hit the token endpoint before the webhook has confirmed payment — the Edge Function sees payment_status = 'pending' and returns 403. The user thinks payment failed.
Fix: On the room page, poll the registration payment_status endpoint up to 5 times with 1-second intervals before showing an error. Alternatively, use Stripe's synchronous PaymentIntent confirmation (not Checkout Session) so payment status is confirmed before the redirect.
Supabase Realtime drops during long workshops
Symptom: Supabase Realtime connections idle-timeout after extended periods (default 30 minutes), and mobile clients reconnect when switching between WiFi and cellular. Without reconnection logic, the chat panel silently stops receiving messages for some attendees mid-workshop.
Fix: Implement Supabase channel reconnection: listen for the CHANNEL_ERROR and TIMED_OUT subscription states and call channel.subscribe() again with exponential backoff. Show a 'Reconnecting to chat...' banner in the UI so attendees know the state.
Max attendees not enforced — room fills past capacity
Symptom: AI tools generate the max_attendees check client-side before the join button or in the registration form — not at token issuance. A user who registered before capacity was reached can still join after the room is full, and concurrent registrations can exceed capacity in a race.
Fix: Enforce capacity in the Supabase Edge Function that issues Daily tokens: SELECT COUNT(*) FROM registrations WHERE workshop_id = $1 AND payment_status = 'confirmed' and compare against max_attendees. Return 409 Conflict if at capacity. Add a unique partial index on registrations to cap concurrent confirmed rows.
Best practices
Create the Daily room when the workshop is scheduled, not at join time — cold room creation takes 500–2,000ms and delays the first attendee's entry
Issue Daily meeting tokens with exp set to 2 hours maximum — a leaked token with no expiry gives permanent access to your room
Send a calendar invite (.ics attachment) in the registration confirmation email using Resend — it is the single highest-impact UX improvement for workshop attendance rates
Always enforce payment confirmation in the Edge Function, never client-side — client-side checks can be bypassed by anyone with browser dev tools
Subscribe to a single Supabase Realtime channel per workshop_id for both chat and polls — avoid per-row subscriptions that multiply connection count with message volume
Store the Daily recording_url in your database immediately when the recording_ready webhook fires — Daily's hosted URLs expire after 30 days without re-hosting
Show a participant count and session duration timer in the host dashboard — hosts lose track of time and attendee drop-off is invisible without it
Test Stripe webhook verification (constructEventAsync in Deno Edge Functions) before launch — a missing raw body causes HMAC signature failure and silently drops payment confirmations
When You Need Custom Development
AI tools get you to a working paid workshop room in 6–12 hours. These are the signals that your requirements have grown past that boundary:
- You need breakout rooms — splitting a 50-person workshop into groups of 5 requires Daily sub-rooms and scoped token logic that AI tools cannot orchestrate reliably
- Whiteboard or collaborative canvas is a core part of the workshop experience — integrating Liveblocks or Tldraw with synchronized Daily video is a multi-week custom build
- Participant gallery exceeds 25 attendees — at this scale, Daily's Prebuilt UI hits rendering limits and you need custom SFU tile management
- You need to integrate with an existing LMS (Teachable, Thinkific, Kajabi) for course completion tracking, certificates, or subscriber-gated access
- HIPAA compliance is required for healthcare workshops — this mandates a BAA with your video provider and specific data handling requirements
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Do I need to build my own video infrastructure?
No. Daily.co handles WebRTC, TURN/STUN servers, bandwidth adaptation, and recording — you call their REST API to create rooms and issue tokens, and embed their React SDK. The free tier gives you 10,000 participant-minutes per month, which covers roughly three 60-minute workshops with 30 attendees before any cost.
How do I prevent uninvited attendees from joining?
Generate Daily meeting tokens in a Supabase Edge Function that first verifies the user has a confirmed registration (payment_status = 'confirmed') for that workshop. The token has a short expiry (2 hours maximum). Without a valid token, Daily will not admit anyone to the room — the room URL alone is not enough to join.
Can I record workshops and sell them as replays?
Yes. Daily's cloud recording stores the session to S3 and fires a recording_ready webhook with the URL. Save that URL to your workshops table and build a /workshops/:id/replay page using React Player. To sell replay access, apply the same Stripe payment gate you use for live attendance — check purchase status before displaying the player.
What is the cost per workshop attendee?
Daily.co charges $0.00099/participant-minute (approx) after the free tier. A 60-minute workshop with 30 attendees consumes 1,800 participant-minutes, costing roughly $1.78 in video costs. At 10 workshops per month with 30 attendees each, that is about $18/month in Daily fees — well below typical ticket revenue.
How do I add a Q&A with audience upvoting?
Add an is_question bool and upvote_count int to your workshop_chat table. Display Q&A entries sorted by upvote_count descending. Handle upvotes via a Supabase RPC function that increments the count atomically to avoid race conditions — do not do a read-then-write from the client. Subscribe to postgres_changes on the workshop_chat table to push upvote count updates to all attendees in real time.
Can I run workshops on both mobile and desktop?
Daily.co's Prebuilt UI works in desktop browsers and Android Chrome. iOS Safari supports WebRTC since iOS 14.3. Mobile screenshare is limited — iOS only allows screenshare through a ReplayKit extension, so hosts presenting slides should join from desktop. For a native mobile experience, the FlutterFlow path with daily_flutter is the better choice.
How do I handle refunds when I cancel a workshop?
Trigger a Stripe refund via the Stripe API in a Supabase Edge Function when the host sets workshop status to 'cancelled'. Loop through all confirmed registrations, call Stripe's refund endpoint for each payment_intent_id, and mark registration payment_status as 'refunded'. Send a Resend email to each registrant confirming the refund. Full refund processing typically appears in 5–10 business days on the attendee's statement.
What is the difference between a webinar tool and a custom workshop feature?
Webinar tools (Zoom Webinars, Riverside) are separate products your users must context-switch to. A custom workshop feature lives inside your app, behind your auth and payment system, with your branding. You keep 100% of ticket revenue (minus Stripe fees), you own the attendee data, and you can build features unique to your audience — like polls that feed into your product's data model or recordings gated behind your subscription.
Need this feature production-ready?
RapidDev builds virtual workshops into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.