A production-ready Zoom App for automating per-round participant exclusions in multi-round meetings. Automatically moves conflicted attendees to the Waiting Room during restricted rounds and admits them back afterward, based on CSV conflict lists.
This app is designed for hosts running long meetings (e.g., 8-hour sessions) with multiple rounds where certain participants must not participate due to conflicts. The app:
- β Parses CSV files with conflict lists (row-based or column-based format)
- β Automatically moves conflicted participants to Waiting Room at round start
- β Admits those same participants back when the round ends
- β Handles 150+ participants with robust retry logic
- β Provides fallback selection for participants without email addresses
- β Persists state in localStorage for resilience across page refreshes
This is a Zoom App that runs inside the Zoom desktop client as a webview and is served by a small Express backend:
- Tech Stack: React + TypeScript + Vite
- State Management: Zustand with localStorage persistence
- CSV Parsing: PapaParse
- Zoom Integration: Zoom Apps SDK (@zoom/appssdk)
- Backend: Express for Zoom app hosting and browser tools (
/register,/getconflicts)
- Node.js 18+ and npm
- Zoom desktop client (Windows/Mac)
- Zoom account with ability to create Zoom Apps
- ngrok (for local development) or static hosting (for production)
# Navigate to project directory
cd zoomapps-sample-js
# Install dependencies
npm install
# Start development server
npm run devThe app will run on http://localhost:3000.
# Build production bundle
npm run build
# Run the server (requires env vars below)
PORT=3000 \
ZM_CLIENT_ID=... \
ZM_CLIENT_SECRET=... \
ZM_REDIRECT_URL=https://your-domain/zoom \
SESSION_SECRET=your-random-string \
node app.js- Zoom App is served at
/zoom(set your Zoom Home URL tohttps://your-domain/zoom). - Browser tools remain at
/registerand/getconflictsand do not load the Zoom SDK.
GET /registerβ Browser page to choose a user fromdata/users.csvand register the Zoom email they'll join with; writes todata/registrants.csv.POST /registerβ Body:given_name, family_name, email, zoom_email, zoom_email_confirm.GET /getconflictsβ Generatesdata/meeting_conflicts.csvby swapping emails indata/pcconflicts.csvwith the matchingzoom_emailfromdata/registrants.csv, then downloads the file.
./scripts/setup.shInstalls Node (via nvm), npm packages, and builds dist/. Afterward, set your env vars and run node app.js as shown above.
# Install ngrok globally (if not already installed)
npm install -g ngrok
# Start ngrok tunnel
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok-free.app)- Go to Zoom App Marketplace
- Click Develop β Build App
- Choose General App (not OAuth or Meeting SDK)
- Fill in basic information:
- App Name: "Waiting Room Manager" (or your choice)
- Company Name: Your organization
- Developer Contact: Your email
-
In the App Credentials tab:
- Note your Client ID and Client Secret (not needed for frontend-only app, but keep them safe)
-
In the Zoom App SDK section:
- Enable Zoom Apps SDK: Toggle ON
- Home URL:
- For dev:
https://your-ngrok-url.ngrok-free.app(from ngrok) - For production:
https://your-deployment-domain.com
- For dev:
- Redirect URL for OAuth: Leave blank (not needed for frontend-only app)
- Add Domain to Allow List:
- Dev:
your-ngrok-url.ngrok-free.app - Production:
your-deployment-domain.com
- Dev:
-
In the Scopes section, ensure the following capabilities are enabled:
- Check "Zoom Apps SDK" is enabled
- The app will request these capabilities at runtime:
getMeetingParticipantsgetMeetingParticipantsEmailputParticipantToWaitingRoomadmitParticipantFromWaitingRoomgetWaitingRoomParticipantsshowNotificationgetMeetingContext
- Go to the Local Test or Activation tab
- Click Add to install the app to your Zoom account
- The app should now appear in your Zoom Apps list
- Start or join a Zoom meeting as host or co-host
- Click Apps button in the meeting toolbar
- Find and click Waiting Room Manager (or your app name)
- The app will open in a side panel
The interface has three main panels:
- Upload CSV: Click to upload your conflict list CSV file
- Parse Summary: Shows rounds found, total emails, format detected
- Rounds Preview: Expandable list of all rounds and their conflict counts
- Select Round: Dropdown to choose which round to run
- Fallback Mode: Toggle to enable manual selection for participants without email
- Start Round: Moves conflicted participants to Waiting Room
- End Round: Admits previously moved participants back to meeting
- Actions Summary: Preview of matched/unmatched participants before execution
- Stats: Real-time success/failure counts
- Log Entries: Chronological list of all actions with status badges
- Retry Failed: Button to retry failed operations (if any)
The app supports two CSV formats:
round_id,email
1,alice@example.com
1,bob@example.com
2,carol@example.com
2,alice@example.com- Each row = one conflict
- Columns:
round_id,email
email,round_1,round_2,round_3
alice@example.com,1,0,1
bob@example.com,1,1,0
carol@example.com,0,1,0- Each row = one participant
- Columns:
email, then round columns - Values:
1= conflict,0= no conflict - Accepted conflict indicators:
1,true,yes,x
- Email normalization: Emails are automatically trimmed and lowercased
- Invalid emails: Skipped with warnings
- Round IDs: Can be strings or numbers (e.g., "Round 1", "1", "morning-session")
- Upload your CSV file
- Select a round from the dropdown
- Click "Start Round (Exclude Conflicted)"
- The app will:
- Fetch current meeting participants
- Match conflict emails to participant UUIDs
- Show a summary of matches/misses
- Move matched participants to Waiting Room (with progress bar)
- Record moved participants for later restoration
Note: Participants without email addresses will appear in the "No Email" section if Fallback Mode is enabled. You can manually select them by display name.
- When the round is complete, click "End Round (Restore Admitted)"
- The app will:
- Only admit participants that were moved by the app for this round
- Skip participants who left the meeting
- Show progress and final success count
- Idempotency: Starting a round twice won't duplicate actions
- Selective Admit: Ending a round only admits participants moved by this app
- Reconciliation: Before admitting, the app checks if participant is actually in waiting room
- Retry Logic: Failed operations are retried up to 3 times with exponential backoff
- Pre-load CSV: Upload and validate your CSV before the meeting starts
- Test First: Run a test meeting with a few participants to verify the CSV
- Monitor Logs: Keep an eye on the Activity Log panel for any failures
- Late Joiners: If someone joins mid-round, you can re-run "Start Round" - it will only move new matches
- Network Issues: If operations fail, use the "Retry Failed Actions" button
Some participants may not expose their email to the Zoom app. Solutions:
- Enable Fallback Mode to manually select by display name
- Ask participants to update their Zoom profile with email
- Use display name matching as a workaround
If a participant leaves during a round:
- They won't be in Waiting Room when you try to admit them
- The app will skip them with a "not in waiting room" message
- No error occurs; this is expected behavior
- All critical state is saved to localStorage
- If you refresh the page, the app will restore:
- Parsed rounds
- Currently moved participants per round
- Selected round
- Action logs are in-memory only and won't persist across refreshes
If the Zoom client restarts:
- You'll need to re-open the app from the Apps menu
- State will be restored from localStorage
- You can continue managing rounds
This app includes additional backend features for participant registration and conflict management.
Endpoint: /register
A web-based registration page where PC members can register their Zoom meeting email.
Features:
- Dropdown list of all PC members from
data/users.csv - Email input with confirmation
- Stores registrations in
data/registrants.csv - Updates existing registrations automatically
Usage: Navigate to http://localhost:3000/register (or your deployed URL) to access the registration form.
Endpoint: /getconflicts
Generates and downloads a meeting_conflicts.csv file by mapping PC member emails to their registered Zoom emails.
What it does:
- Reads
data/pcconflicts.csv(original PC conflicts) - Reads
data/registrants.csv(registered Zoom emails) - Replaces PC emails with Zoom emails based on registration mapping
- Returns the generated CSV file for download
Usage:
- Visit
http://localhost:3000/getconflictsin a browser to download the file - Or use the Python script directly:
python scripts/python/generate_meeting_conflicts.py
Data Flow:
users.csv β Registration Form β registrants.csv
β
pcconflicts.csv + registrants.csv β meeting_conflicts.csv (Zoom-ready)
Located in scripts/python/:
- generate_meeting_conflicts.py: Standalone script to generate meeting conflicts CSV
- Can be run independently:
python scripts/python/generate_meeting_conflicts.py - Also triggered by the
/getconflictsAPI endpoint
- Can be run independently:
See scripts/python/README.md for details.
If you're unsure which participants were moved by the app:
- Check the localStorage state (browser dev tools β Application β Local Storage)
- Look for
zoom-waiting-room-manager-state - The
activeRoundsobject containsmovedParticipantsarrays per round
If you need to manually override:
- Use Zoom's built-in Waiting Room controls (Security β Participants β Waiting Room)
- The app won't interfere with manual admissions
- When you "End Round", the app will only try to admit its tracked participants
If you uploaded a bad CSV:
- The parse summary will show errors and warnings
- Upload a corrected CSV - it will replace the previous data
- Re-select your round and proceed
# Install Vercel CLI
npm install -g vercel
# Build the app
npm run build
# Deploy
vercel --prod
# Copy the production URL (e.g., https://your-app.vercel.app)# Build the app
npm run build
# Drag and drop the 'dist' folder to Netlify
# Or use Netlify CLI
npm install -g netlify-cli
netlify deploy --prod --dir=dist# Build the app
npm run build
# Upload the 'dist' folder to Cloudflare Pages
# Configure build settings:
# - Build command: npm run build
# - Publish directory: distAfter deploying:
- Go to your Zoom App settings in Marketplace
- Update Home URL to your production URL
- Update Domain Allow List to match
- Save and test
# Start dev server with hot reload
npm run dev
# Run in Zoom client (requires ngrok tunnel)
# 1. Start ngrok: ngrok http 3000
# 2. Update Zoom App Home URL to ngrok URL
# 3. Open app from Zoom meeting# TypeScript check + build
npm run build
# Preview production build locally
npm run previewnpm run lint- Concurrency: Waiting room operations are batched with a limit of 5 concurrent requests
- Retry Logic: Failed operations retry up to 3 times with exponential backoff (300ms, 900ms, 2700ms)
- Large Meetings: Tested with 150+ participants; operations complete in under 30 seconds
- CSV Size: Can handle CSVs with thousands of rows; parsing is fast (< 1 second)
- No Data Upload: All CSV processing happens in the browser
- localStorage Only: State is stored locally in the browser, not on any server
- Email Privacy: Participant emails are only used for matching; not stored or transmitted
- Zoom APIs Only: No external API calls except to Zoom
Q: Can I use this without email addresses?
A: Yes, enable Fallback Mode and manually select participants by display name.
Q: What happens if I close the app mid-round?
A: State is saved in localStorage. Re-open the app and click "End Round" to restore participants.
Q: Can co-hosts use this app?
A: Yes, if they have waiting room permissions. The app checks for host/co-host role.
Q: Does this work with Zoom webinars?
A: This is designed for meetings. Webinars have different participant management.
Q: Can I edit rounds mid-meeting?
A: Yes, upload a new CSV anytime. It will replace the previous data.
For issues or questions:
- Check the Activity Log panel for error details
- Review this README and Operator Guide
- Check browser console for technical errors (F12 β Console)
src/
βββ sdk/
β βββ zoom.ts # Zoom SDK wrapper with typed methods
βββ csv/
β βββ parse.ts # CSV parsing (row & column formats)
βββ operations/
β βββ matching.ts # Participant matching logic
β βββ waitingRoom.ts # Waiting room ops with retry
βββ state/
β βββ store.ts # Zustand store + localStorage
βββ components/
β βββ UploadPanel.tsx # CSV upload & preview
β βββ RoundSelector.tsx # Round dropdown
β βββ ControlPanel.tsx # Start/End round controls
β βββ LogPanel.tsx # Activity log
βββ types.ts # TypeScript type definitions
βββ utils.ts # Utility functions
βββ App.tsx # Main app component
βββ main.tsx # Entry point
βββ index.css # Tailwind + custom styles
ISC License
Built with β€οΈ for seamless Zoom meeting management