From 807819bb2fdcc2b46946f68d133249ff097e3b6b Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Wed, 8 Oct 2025 13:06:04 -0400 Subject: [PATCH] llm instructions --- CODER_BRIEFING.md | 345 ++++++ IMPLEMENTATION_GUIDE.md | 489 ++++++++ PLAN.md | 2452 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 3286 insertions(+) create mode 100644 CODER_BRIEFING.md create mode 100644 IMPLEMENTATION_GUIDE.md create mode 100644 PLAN.md diff --git a/CODER_BRIEFING.md b/CODER_BRIEFING.md new file mode 100644 index 00000000..18766df1 --- /dev/null +++ b/CODER_BRIEFING.md @@ -0,0 +1,345 @@ +# Multi-Provider Video Platform Implementation - Coder Briefing + +## Your Mission + +Implement multi-provider video platform support in Reflector, allowing the system to work with both Whereby and Daily.co video conferencing providers. The goal is to abstract the current Whereby-only implementation and add Daily.co as a second provider, with the ability to switch between them via environment variables. + +**Branch:** `igor/dailico-2` (you're already on it) + +**Estimated Time:** 12-16 hours (senior engineer) + +**Complexity:** Medium-High (requires careful integration with existing codebase) + +--- + +## What You Have + +### 1. **PLAN.md** - Your Technical Specification (2,452 lines) + - Complete step-by-step implementation guide + - All code examples you need + - Architecture diagrams and design rationale + - Testing strategy and success metrics + - **Read this first** to understand the overall approach + +### 2. **IMPLEMENTATION_GUIDE.md** - Your Practical Guide + - What to copy vs. adapt vs. rewrite + - Common pitfalls and how to avoid them + - Verification checklists for each phase + - Decision trees for implementation choices + - **Use this as your day-to-day reference** + +### 3. **Reference Implementation** - `./reflector-dailyco-reference/` + - Working implementation from 2.5 months ago + - Good architecture and patterns + - **BUT:** 91 commits behind current main, DO NOT merge directly + - Use for inspiration and code patterns only + +--- + +## Critical Context: Why Not Just Merge? + +The reference branch (`origin/igor/feat-dailyco`) was started on August 1, 2025 and is now severely diverged from main: + +- **91 commits behind main** +- Main has 12x more changes (45,840 insertions vs 3,689) +- Main added: calendar integration, webhooks, full-text search, React Query migration, security fixes +- Reference removed: features that main still has and needs + +**Merging would be a disaster.** We're implementing fresh on current main, using the reference for validated patterns. + +--- + +## High-Level Approach + +### Phase 1: Analysis (2 hours) +- Study current Whereby integration +- Define abstraction requirements +- Create standard data models + +### Phase 2: Abstraction Layer (4-5 hours) +- Build platform abstraction (base class, registry, factory) +- Extract Whereby into the abstraction +- Update database schema (add `platform` field) +- Integrate into rooms.py **without breaking calendar/webhooks** + +### Phase 3: Daily.co Implementation (4-5 hours) +- Implement Daily.co client +- Add webhook handler +- Create frontend components (rewrite API calls for React Query) +- Add recording processing + +### Phase 4: Testing (2-3 hours) +- Unit tests for platform abstraction +- Integration tests for webhooks +- Manual testing with both providers + +--- + +## Key Files You'll Touch + +### Backend (New) +``` +server/reflector/video_platforms/ +├── __init__.py +├── base.py ← Abstract base class +├── models.py ← Platform, MeetingData, VideoPlatformConfig +├── registry.py ← Platform registration system +├── factory.py ← Client creation and config +├── whereby.py ← Whereby client wrapper +├── daily.py ← Daily.co client +└── mock.py ← Mock client for testing + +server/reflector/views/daily.py ← Daily.co webhooks +server/tests/test_video_platforms.py ← Platform tests +server/tests/test_daily_webhook.py ← Webhook tests +``` + +### Backend (Modified - Careful!) +``` +server/reflector/settings.py ← Add Daily.co settings +server/reflector/db/rooms.py ← Add platform field, PRESERVE calendar fields +server/reflector/db/meetings.py ← Add platform field +server/reflector/views/rooms.py ← Integrate abstraction, PRESERVE calendar/webhooks +server/reflector/worker/process.py ← Add process_recording_from_url task +server/reflector/app.py ← Register daily router +server/env.example ← Document new env vars +``` + +### Frontend (New) +``` +www/app/[roomName]/components/ +├── RoomContainer.tsx ← Platform router +├── DailyRoom.tsx ← Daily.co component (rewrite API calls!) +└── WherebyRoom.tsx ← Extract existing logic +``` + +### Frontend (Modified) +``` +www/app/[roomName]/page.tsx ← Use RoomContainer +www/package.json ← Add @daily-co/daily-js +``` + +### Database +``` +server/migrations/versions/XXXXXX_add_platform_support.py ← Generate fresh migration +``` + +--- + +## Critical Warnings ⚠️ + +### 1. **DO NOT Copy Database Migrations** +The reference migration has the wrong `down_revision` and is based on old schema. +```bash +# Instead: +cd server +uv run alembic revision -m "add_platform_support" +# Then edit the generated file +``` + +### 2. **DO NOT Remove Main's Features** +Main has calendar integration, webhooks, ICS sync that reference doesn't have. +When modifying `rooms.py`, only change meeting creation logic, preserve everything else. + +### 3. **DO NOT Copy Frontend API Calls** +Reference uses old OpenAPI client. Main uses React Query. +Check how main currently makes API calls and replicate that pattern. + +### 4. **DO NOT Copy package.json/migrations** +These files are severely outdated in reference. + +### 5. **Preserve Type Safety** +Use `TYPE_CHECKING` imports to avoid circular dependencies: +```python +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from reflector.db.rooms import Room +``` + +--- + +## How to Start + +### Day 1 Morning: Setup & Understanding (2-3 hours) +```bash +# 1. Verify you're on the right branch +git branch +# Should show: igor/dailico-2 + +# 2. Read the docs (in order) +# - PLAN.md (skim to understand scope, read Phase 1 carefully) +# - IMPLEMENTATION_GUIDE.md (read fully, bookmark it) + +# 3. Study current Whereby integration +cat server/reflector/views/rooms.py | grep -A 20 "whereby" +cat www/app/[roomName]/page.tsx + +# 4. Check reference implementation structure +ls -la reflector-dailyco-reference/server/reflector/video_platforms/ +``` + +### Day 1 Afternoon: Phase 1 Execution (2-3 hours) +```bash +# 5. Copy video_platforms directory from reference +cp -r reflector-dailyco-reference/server/reflector/video_platforms/ \ + server/reflector/ + +# 6. Review and fix imports +cd server +uv run ruff check reflector/video_platforms/ + +# 7. Add settings to settings.py (see PLAN.md Phase 2.7) + +# 8. Test imports work +uv run python -c "from reflector.video_platforms import create_platform_client; print('OK')" +``` + +### Day 2: Phase 2 - Database & Integration (4-5 hours) +```bash +# 9. Generate migration +uv run alembic revision -m "add_platform_support" +# Edit the file following PLAN.md Phase 2.8 + +# 10. Update Room/Meeting models +# Add platform field, PRESERVE all existing fields + +# 11. Integrate into rooms.py +# Carefully modify meeting creation, preserve calendar/webhooks + +# 12. Add Daily.co webhook handler +cp reflector-dailyco-reference/server/reflector/views/daily.py \ + server/reflector/views/ +# Register in app.py +``` + +### Day 3: Phase 3 - Frontend & Testing (4-5 hours) +```bash +# 13. Create frontend components +mkdir -p www/app/[roomName]/components + +# 14. Add Daily.co dependency +cd www +pnpm add @daily-co/daily-js@^0.81.0 + +# 15. Create RoomContainer, DailyRoom, WherebyRoom +# IMPORTANT: Rewrite API calls using React Query patterns + +# 16. Regenerate types +pnpm openapi + +# 17. Copy and adapt tests +cp reflector-dailyco-reference/server/tests/test_*.py server/tests/ + +# 18. Run tests +cd server +REDIS_HOST=localhost \ +CELERY_BROKER_URL=redis://localhost:6379/1 \ +uv run pytest tests/test_video_platforms.py -v +``` + +--- + +## Verification Checklist + +After implementation, all of these must pass: + +**Backend:** +- [ ] `cd server && uv run ruff check .` passes +- [ ] `uv run alembic upgrade head` works cleanly +- [ ] `uv run pytest tests/test_video_platforms.py` passes +- [ ] Can import: `from reflector.video_platforms import create_platform_client` +- [ ] Settings has all Daily.co variables + +**Frontend:** +- [ ] `cd www && pnpm lint` passes +- [ ] No TypeScript errors +- [ ] `pnpm openapi` generates platform field +- [ ] No `@ts-ignore` for platform field + +**Integration:** +- [ ] Whereby meetings still work (existing flow unchanged) +- [ ] Calendar/webhook features still work in rooms.py +- [ ] env.example documents all new variables + +--- + +## When You're Stuck + +### Check These Resources: +1. **PLAN.md** - Detailed code examples for your exact scenario +2. **IMPLEMENTATION_GUIDE.md** - Common pitfalls section +3. **Reference code** - See how it was solved before +4. **Git diff** - Compare reference to your implementation + +### Compare Files: +```bash +# See what reference did +diff reflector-dailyco-reference/server/reflector/views/rooms.py \ + server/reflector/views/rooms.py + +# See what changed in main since reference branch +git log --oneline --since="2025-08-01" -- server/reflector/views/rooms.py +``` + +### Common Issues: +- **Circular imports:** Use `TYPE_CHECKING` pattern +- **Tests fail with postgres error:** Use `REDIS_HOST=localhost` env vars +- **Frontend API calls broken:** Check current React Query patterns in main +- **Migrations fail:** Ensure you generated fresh, not copied + +--- + +## Success Looks Like + +When you're done: +- ✅ All tests pass +- ✅ Linting passes +- ✅ Can create Whereby meetings (unchanged behavior) +- ✅ Can create Daily.co meetings (with env vars) +- ✅ Calendar/webhooks still work +- ✅ Frontend has no TypeScript errors +- ✅ Platform selection via environment variables works + +--- + +## Communication + +If you need clarification on requirements, have questions about architecture decisions, or find issues with the spec, document them clearly with: +- What you expected +- What you found +- Your proposed solution + +The PLAN.md document is comprehensive but you may find edge cases. Use your engineering judgment and document decisions. + +--- + +## Final Notes + +**This is not a simple copy-paste job.** You're doing careful integration work where you need to: +- Understand the abstraction pattern (PLAN.md) +- Preserve all of main's features +- Adapt reference code to current patterns +- Think about edge cases and testing + +Take your time with Phase 2 (rooms.py integration) - that's where most bugs will come from if you accidentally break calendar/webhook features. + +**Good luck! You've got comprehensive specs, working reference code, and a clean starting point. You can do this.** + +--- + +## Quick Reference + +```bash +# Your workspace +├── PLAN.md ← Complete technical spec (read first) +├── IMPLEMENTATION_GUIDE.md ← Practical guide (bookmark this) +├── CODER_BRIEFING.md ← This file +└── reflector-dailyco-reference/ ← Reference implementation (inspiration only) + +# Key commands +cd server && uv run ruff check . # Lint backend +cd www && pnpm lint # Lint frontend +cd server && uv run alembic revision -m "..." # Create migration +cd www && pnpm openapi # Regenerate types +cd server && uv run pytest -v # Run tests +``` diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md new file mode 100644 index 00000000..11e1fee5 --- /dev/null +++ b/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,489 @@ +# Daily.co Implementation Guide + +## Overview +Implement multi-provider video platform support (Whereby + Daily.co) following PLAN.md. + +## Reference Code Location +- **Reference branch:** `origin/igor/feat-dailyco` (on remote) +- **Worktree location:** `./reflector-dailyco-reference/` +- **Status:** Reference only - DO NOT merge or copy directly + +## What Exists in Reference Branch (For Inspiration) + +### ✅ Can Use As Reference (Well-Implemented) +``` +server/reflector/video_platforms/ +├── base.py ← Platform abstraction (good design, copy-safe) +├── models.py ← Data models (copy-safe) +├── registry.py ← Registry pattern (copy-safe) +├── factory.py ← Factory pattern (needs settings updates) +├── whereby.py ← Whereby client (needs adaptation) +├── daily.py ← Daily.co client (needs adaptation) +└── mock.py ← Mock client (copy-safe for tests) + +server/reflector/views/daily.py ← Webhook handler (needs adaptation) +server/tests/test_video_platforms.py ← Tests (good reference) +server/tests/test_daily_webhook.py ← Tests (good reference) + +www/app/[roomName]/components/ +├── RoomContainer.tsx ← Platform router (needs React Query) +├── DailyRoom.tsx ← Daily component (needs React Query) +└── WherebyRoom.tsx ← Whereby extraction (needs React Query) +``` + +### ⚠️ Needs Significant Changes (Use Logic Only) +- `server/reflector/db/rooms.py` - Reference removed calendar/webhook fields that main has +- `server/reflector/db/meetings.py` - Same issue (missing user_id handling differences) +- `server/reflector/views/rooms.py` - Main has calendar integration, webhooks, ICS sync +- `server/reflector/worker/process.py` - Main has different recording flow +- Migration files - Must regenerate against current main schema + +### ❌ Do NOT Use (Outdated/Incompatible) +- `package.json`/`pnpm-lock.yaml` - Main uses different dependency versions +- Frontend API client calls - Main uses React Query (reference uses old OpenAPI client) +- Database migrations - Must create new ones from scratch +- Any files that delete features present in main (search, calendar, webhooks) + +## Key Differences: Reference vs Current Main + +| Aspect | Reference Branch | Current Main | Action Required | +|--------|------------------|--------------|-----------------| +| **API client** | Old OpenAPI generated | React Query hooks | Rewrite all API calls | +| **Database schema** | Simplified (removed features) | Has calendar, webhooks, full-text search | Merge carefully, preserve main features | +| **Settings** | Aug 2025 structure | Current structure | Adapt carefully | +| **Migrations** | Branched from Aug 1 | Current main (91+ commits ahead) | Regenerate from scratch | +| **Frontend deps** | `@daily-co/daily-js@0.81.0` | Check current versions | Update to compatible versions | +| **Package manager** | yarn | pnpm (maybe both?) | Use what main uses | + +## Branch Divergence Analysis + +**The reference branch is 91 commits behind main and severely diverged:** +- Reference: 8 commits, 3,689 insertions, 425 deletions +- Main since divergence: 320 files changed, 45,840 insertions, 16,827 deletions +- **Main has 12x more changes** + +**Major features in main that reference lacks:** +1. Calendar integration (ICS sync with rooms) +2. Self-hosted GPU API infrastructure +3. Frontend OpenAPI React Query migration +4. Full-text search (backend + frontend) +5. Webhook system for room events +6. Environment variable migration +7. Security fixes and auth improvements +8. Docker production frontend +9. Meeting user ID removal (schema change) +10. NextJS version upgrades + +**High conflict risk files:** +- `server/reflector/views/rooms.py` - 12x more changes in main +- `server/reflector/db/rooms.py` - Main added 7+ fields +- `www/package.json` - NextJS major version bump +- Database migrations - 20+ new migrations in main + +## Implementation Approach + +### Phase 1: Copy Clean Abstractions (1-2 hours) + +**Files to copy directly from reference:** +```bash +# Core abstraction (review but mostly safe to copy) +cp -r reflector-dailyco-reference/server/reflector/video_platforms/ \ + server/reflector/ + +# BUT review each file for: +# - Import paths (make sure they match current main) +# - Settings references (adapt to current settings.py) +# - Type imports (ensure no circular dependencies) +``` + +**After copying, immediately:** +```bash +cd server +# Check for issues +uv run ruff check reflector/video_platforms/ +# Fix any import errors or type issues +``` + +### Phase 2: Adapt to Current Main (2-3 hours) + +**2.1 Settings Integration** + +File: `server/reflector/settings.py` + +Add at the appropriate location (near existing Whereby settings): + +```python +# Daily.co API Integration (NEW) +DAILY_API_KEY: str | None = None +DAILY_WEBHOOK_SECRET: str | None = None +DAILY_SUBDOMAIN: str | None = None +AWS_DAILY_S3_BUCKET: str | None = None +AWS_DAILY_S3_REGION: str = "us-west-2" +AWS_DAILY_ROLE_ARN: str | None = None + +# Platform Migration Feature Flags (NEW) +DAILY_MIGRATION_ENABLED: bool = False # Conservative default +DAILY_MIGRATION_ROOM_IDS: list[str] = [] +DEFAULT_VIDEO_PLATFORM: Literal["whereby", "daily"] = "whereby" +``` + +**2.2 Database Migration** + +⚠️ **CRITICAL: Do NOT copy migration from reference** + +Generate new migration: +```bash +cd server +uv run alembic revision -m "add_platform_support" +``` + +Edit the generated migration file to add `platform` column: +```python +def upgrade(): + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.add_column( + sa.Column("platform", sa.String(), nullable=False, server_default="whereby") + ) + + with op.batch_alter_table("meeting", schema=None) as batch_op: + batch_op.add_column( + sa.Column("platform", sa.String(), nullable=False, server_default="whereby") + ) +``` + +**2.3 Update Database Models** + +File: `server/reflector/db/rooms.py` + +Add platform field (preserve all existing fields from main): +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from reflector.video_platforms.models import Platform + +class Room: + # ... ALL existing fields from main (calendar, webhooks, etc.) ... + + # NEW: Platform field + platform: "Platform" = sqlalchemy.Column( + sqlalchemy.String, + nullable=False, + server_default="whereby", + ) +``` + +File: `server/reflector/db/meetings.py` + +Same approach - add platform field, preserve everything from main. + +**2.4 Integrate Platform Abstraction into rooms.py** + +⚠️ **This is the most delicate part - main has calendar/webhook features** + +File: `server/reflector/views/rooms.py` + +Strategy: +1. Add imports at top +2. Modify meeting creation logic only +3. Preserve all calendar/webhook/ICS logic from main + +```python +# Add imports +from reflector.video_platforms import ( + create_platform_client, + get_platform_for_room, +) + +# In create_meeting endpoint: +# OLD: Direct Whereby API calls +# NEW: Platform abstraction + +# Find the meeting creation section and replace: +platform = get_platform_for_room(room.id) +client = create_platform_client(platform) + +meeting_data = await client.create_meeting( + room_name_prefix=room.name, + end_date=meeting_data.end_date, + room=room, +) + +# Then create Meeting record with meeting_data.platform, meeting_data.meeting_id, etc. +``` + +**2.5 Add Daily.co Webhook Handler** + +Copy from reference, minimal changes needed: +```bash +cp reflector-dailyco-reference/server/reflector/views/daily.py \ + server/reflector/views/ +``` + +Register in `server/reflector/app.py`: +```python +from reflector.views import daily + +app.include_router(daily.router, prefix="/v1/daily", tags=["daily"]) +``` + +**2.6 Add Recording Processing Task** + +File: `server/reflector/worker/process.py` + +Add the `process_recording_from_url` task from reference (copy the function). + +### Phase 3: Frontend Adaptation (3-4 hours) + +**3.1 Determine Current API Client Pattern** + +First, check how main currently makes API calls: +```bash +cd www +grep -r "api\." app/ | head -20 +# Look for patterns like: api.v1Something() +``` + +**3.2 Create Components** + +Copy component structure from reference but **rewrite all API calls**: + +```bash +mkdir -p www/app/[roomName]/components +``` + +Files to create: +- `RoomContainer.tsx` - Platform router (mostly copy-safe, just fix imports) +- `DailyRoom.tsx` - Needs React Query API calls +- `WherebyRoom.tsx` - Extract current room page logic + +**Example React Query pattern** (adapt to your actual API): +```typescript +import { api } from '@/app/api/client' + +// In DailyRoom.tsx +const handleConsent = async () => { + try { + await api.v1MeetingAudioConsent({ + path: { meeting_id: meeting.id }, + body: { consent: true }, + }) + // ... + } catch (error) { + // ... + } +} +``` + +**3.3 Add Daily.co Dependency** + +Check current package manager: +```bash +cd www +ls package-lock.json yarn.lock pnpm-lock.yaml +``` + +Then install: +```bash +# If using pnpm +pnpm add @daily-co/daily-js@^0.81.0 + +# If using yarn +yarn add @daily-co/daily-js@^0.81.0 +``` + +**3.4 Update TypeScript Types** + +After backend changes, regenerate types: +```bash +cd www +pnpm openapi # or yarn openapi +``` + +This should pick up the new `platform` field on Meeting type. + +### Phase 4: Testing (2-3 hours) + +**4.1 Copy Test Structure** + +```bash +cp reflector-dailyco-reference/server/tests/test_video_platforms.py \ + server/tests/ + +cp reflector-dailyco-reference/server/tests/test_daily_webhook.py \ + server/tests/ +``` + +**4.2 Fix Test Imports and Fixtures** + +Update imports to match current test infrastructure: +- Check `server/tests/conftest.py` for fixture patterns +- Update database access patterns if changed +- Fix any import errors + +**4.3 Run Tests** + +```bash +cd server +# Run with environment variables for Mac +REDIS_HOST=localhost \ +CELERY_BROKER_URL=redis://localhost:6379/1 \ +CELERY_RESULT_BACKEND=redis://localhost:6379/1 \ +uv run pytest tests/test_video_platforms.py -v +``` + +### Phase 5: Environment Configuration + +**Update `server/env.example`:** + +Add at the end: +```bash +# Daily.co API Integration +DAILY_API_KEY=your-daily-api-key +DAILY_WEBHOOK_SECRET=your-daily-webhook-secret +DAILY_SUBDOMAIN=your-subdomain +AWS_DAILY_S3_BUCKET=your-daily-bucket +AWS_DAILY_S3_REGION=us-west-2 +AWS_DAILY_ROLE_ARN=arn:aws:iam::ACCOUNT:role/DailyRecording + +# Platform Selection +DAILY_MIGRATION_ENABLED=false # Master switch +DAILY_MIGRATION_ROOM_IDS=[] # Specific room IDs +DEFAULT_VIDEO_PLATFORM=whereby # Default platform +``` + +## Decision Tree: Copy vs Adapt vs Rewrite + +``` +┌─ Is it pure abstraction logic? (base.py, registry.py, models.py) +│ YES → Copy directly, review imports +│ NO → Continue ↓ +│ +├─ Does it touch database models? +│ YES → Adapt carefully, preserve main's fields +│ NO → Continue ↓ +│ +├─ Does it make API calls on frontend? +│ YES → Rewrite using React Query +│ NO → Continue ↓ +│ +├─ Is it a database migration? +│ YES → Generate fresh from current schema +│ NO → Continue ↓ +│ +└─ Does it touch rooms.py or core business logic? + YES → Merge carefully, preserve calendar/webhooks + NO → Safe to adapt from reference +``` + +## Verification Checklist + +After each phase, verify: + +**Phase 1 (Abstraction Layer):** +- [ ] `uv run ruff check server/reflector/video_platforms/` passes +- [ ] No circular import errors +- [ ] Can import `from reflector.video_platforms import create_platform_client` + +**Phase 2 (Backend Integration):** +- [ ] `uv run ruff check server/` passes +- [ ] Migration file generated (not copied) +- [ ] Room and Meeting models have platform field +- [ ] rooms.py still has calendar/webhook features + +**Phase 3 (Frontend):** +- [ ] `pnpm lint` passes +- [ ] No TypeScript errors +- [ ] No `@ts-ignore` for platform field +- [ ] API calls use React Query patterns + +**Phase 4 (Testing):** +- [ ] Tests can be collected: `pytest tests/test_video_platforms.py --collect-only` +- [ ] Database fixtures work +- [ ] Mock platform works + +**Phase 5 (Config):** +- [ ] env.example has Daily.co variables +- [ ] settings.py has all new variables +- [ ] No duplicate variable definitions + +## Common Pitfalls + +### 1. Database Schema Conflicts +**Problem:** Reference removed fields that main has (calendar, webhooks) +**Solution:** Always preserve main's fields, only add platform field + +### 2. Migration Conflicts +**Problem:** Reference migration has wrong `down_revision` +**Solution:** Always generate fresh migration from current main + +### 3. Frontend API Calls +**Problem:** Reference uses old API client patterns +**Solution:** Check current main's API usage, replicate that pattern + +### 4. Import Errors +**Problem:** Circular imports with TYPE_CHECKING +**Solution:** Use `if TYPE_CHECKING:` for Room/Meeting imports in video_platforms + +### 5. Test Database Issues +**Problem:** Tests fail with "could not translate host name 'postgres'" +**Solution:** Use environment variables: `REDIS_HOST=localhost DATABASE_URL=...` + +### 6. Preserved Features Broken +**Problem:** Calendar/webhook features stop working +**Solution:** Carefully review rooms.py diff, only change meeting creation, not calendar logic + +## File Modification Summary + +**New files (can copy):** +- `server/reflector/video_platforms/*.py` (entire directory) +- `server/reflector/views/daily.py` +- `server/tests/test_video_platforms.py` +- `server/tests/test_daily_webhook.py` +- `www/app/[roomName]/components/RoomContainer.tsx` +- `www/app/[roomName]/components/DailyRoom.tsx` +- `www/app/[roomName]/components/WherebyRoom.tsx` + +**Modified files (careful merging):** +- `server/reflector/settings.py` - Add Daily.co settings +- `server/reflector/db/rooms.py` - Add platform field +- `server/reflector/db/meetings.py` - Add platform field +- `server/reflector/views/rooms.py` - Integrate platform abstraction +- `server/reflector/worker/process.py` - Add process_recording_from_url +- `server/reflector/app.py` - Register daily router +- `server/env.example` - Add Daily.co variables +- `www/app/[roomName]/page.tsx` - Use RoomContainer +- `www/package.json` - Add @daily-co/daily-js + +**Generated files (do not copy):** +- `server/migrations/versions/XXXXXX_add_platform_support.py` - Generate fresh + +## Success Metrics + +Implementation is complete when: +- [ ] All tests pass (including new platform tests) +- [ ] Linting passes (ruff, pnpm lint) +- [ ] Migration applies cleanly: `uv run alembic upgrade head` +- [ ] Can create Whereby meeting (existing flow unchanged) +- [ ] Can create Daily.co meeting (with env vars set) +- [ ] Frontend loads without TypeScript errors +- [ ] No features from main were accidentally removed + +## Getting Help + +**Reference documentation locations:** +- Implementation plan: `PLAN.md` +- Reference implementation: `./reflector-dailyco-reference/` +- Current main codebase: `./ ` (current directory) + +**Compare implementations:** +```bash +# Compare specific files +diff reflector-dailyco-reference/server/reflector/video_platforms/base.py \ + server/reflector/video_platforms/base.py + +# See what changed in rooms.py between reference branch point and now +git log --oneline --since="2025-08-01" -- server/reflector/views/rooms.py +``` + +**Key insight:** The reference branch validates the approach and provides working code patterns, but you're implementing fresh against current main to avoid merge conflicts and preserve all new features. diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..6ab90da6 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,2452 @@ +# Technical Specification: Multi-Provider Video Platform Integration + +**Version:** 1.0 +**Status:** Ready for Implementation +**Target:** Steps 1-3 of Video Platform Migration +**Estimated Effort:** 12-16 hours (senior engineer) +**Branch Base:** Current `main` branch + +--- + +## Executive Summary + +This document provides a comprehensive technical specification for implementing multi-provider video platform support in Reflector, focusing on abstracting the existing Whereby integration and adding Daily.co as a second provider. The implementation follows a phased approach ensuring zero downtime, backward compatibility, and feature parity between providers. + +**Scope:** +- **Phase 1:** Extract existing Whereby implementation into reusable patterns +- **Phase 2:** Create provider abstraction layer maintaining current functionality +- **Phase 3:** Implement Daily.co provider with feature parity to Whereby + +**Out of Scope:** +- Multi-track audio processing (Phase 4 - future work) +- Jitsi integration (Phase 5 - future work) +- Platform selection UI (controlled via environment variables) +- Advanced Daily.co features (presence API, raw-tracks recording) + +--- + +## Business Context + +### Problem Statement + +Reflector currently has a hard dependency on Whereby for video conferencing. This creates: +1. **Vendor lock-in risk** - Single point of failure for core functionality +2. **Cost optimization limitations** - Cannot leverage competitive pricing +3. **Feature constraints** - Limited to Whereby's feature set +4. **Scalability concerns** - Dependent on Whereby's infrastructure reliability + +### Business Goals + +1. **Risk Mitigation:** Enable platform switching without code changes +2. **Cost Flexibility:** Allow deployment-specific provider selection +3. **Feature Expansion:** Prepare for future multi-track diarization (Daily.co raw-tracks) +4. **Architectural Cleanliness:** Establish patterns for future provider additions (Jitsi) + +### Success Criteria + +- ✅ Existing Whereby installations continue working unchanged +- ✅ New installations can choose Daily.co via environment variable +- ✅ Zero data migration required for existing deployments +- ✅ Recording processing pipeline unchanged +- ✅ Transcription quality identical between providers +- ✅ <2% performance overhead from abstraction layer +- ✅ Test coverage >90% for platform abstraction + +--- + +## Architecture Overview + +### Current Architecture (Whereby-only) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Frontend │ +│ └─ RoomPage.tsx │ +│ └─ web component │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Backend │ +│ └─ rooms.py (direct Whereby API calls) │ +│ └─ whereby.py (webhook handler) │ +│ └─ process.py (S3-based recording ingestion) │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ External Services │ +│ ├─ Whereby API │ +│ ├─ Whereby Webhooks → SQS → recordings │ +│ └─ S3 (Whereby uploads directly) │ +└─────────────────────────────────────────────────────────┘ +``` + +### Target Architecture (Multi-provider) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Frontend │ +│ └─ RoomContainer.tsx (platform router) │ +│ ├─ WherebyRoom.tsx │ +│ └─ DailyRoom.tsx │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Backend - Platform Abstraction Layer │ +│ ├─ VideoPlatformClient (ABC) │ +│ ├─ PlatformFactory │ +│ ├─ PlatformRegistry │ +│ └─ Platform Implementations: │ +│ ├─ WherebyClient │ +│ ├─ DailyClient │ +│ └─ MockClient (testing) │ +└─────────────────────────────────────────────────────────┘ + │ + ┌──────────┴──────────┐ + ▼ ▼ +┌───────────────────────┐ ┌──────────────────────┐ +│ Whereby Services │ │ Daily.co Services │ +│ ├─ Whereby API │ │ ├─ Daily.co API │ +│ ├─ Webhooks │ │ ├─ Webhooks │ +│ └─ S3 Direct Upload │ │ └─ Download URLs │ +└───────────────────────┘ └──────────────────────┘ +``` + +### Key Architectural Principles + +1. **Single Responsibility:** Each provider implements only platform-specific logic +2. **Open/Closed:** New providers can be added without modifying existing code +3. **Liskov Substitution:** All providers are interchangeable via base interface +4. **Dependency Inversion:** Business logic depends on abstraction, not implementations +5. **Interface Segregation:** Platform interface contains only universally needed methods + +--- + +## Phase 1: Analysis & Extraction (2 hours) + +### Objective +Understand current Whereby implementation patterns to inform abstraction design. + +### Step 1.1: Audit Current Whereby Implementation + +**Files to analyze:** + +```bash +# Backend +server/reflector/views/rooms.py # Room/meeting creation logic +server/reflector/views/whereby.py # Webhook handler +server/reflector/worker/process.py # Recording processing + +# Frontend +www/app/[roomName]/page.tsx # Room page component +www/app/(app)/rooms/page.tsx # Room creation form + +# Database +server/reflector/db/rooms.py # Room model +server/reflector/db/meetings.py # Meeting model +server/reflector/db/recordings.py # Recording model +``` + +**Create analysis document:** + +```markdown +# Whereby Integration Analysis + +## API Calls Made +1. Create meeting: POST to whereby.dev/v1/meetings +2. Required fields: endDate, roomMode, fields +3. Response structure: { meetingId, roomUrl, hostRoomUrl } + +## Webhook Events Received +1. room.client.joined - participant count++ +2. room.client.left - participant count-- + +## Recording Flow +1. Whereby uploads MP4 to S3 bucket (direct) +2. S3 event → SQS queue +3. Worker polls SQS → downloads from S3 +4. Processing pipeline: transcription → diarization → summarization + +## Data Stored +- Room: whereby-specific fields (if any) +- Meeting: meetingId, roomUrl, hostRoomUrl +- Recording: S3 bucket, object_key + +## Frontend Integration +- web component +- SDK loaded via dynamic import +- Custom focus management for consent dialog +- Events: leave, ready +``` + +### Step 1.2: Identify Abstraction Points + +**Create abstraction requirements document:** + +```markdown +# Platform Abstraction Requirements + +## Must Abstract +1. Meeting creation (different APIs, different request/response formats) +2. Webhook signature verification (different algorithms/formats) +3. Recording ingestion (S3 direct vs download URL) +4. Frontend room component (web component vs iframe) + +## Can Remain Concrete +1. Recording processing pipeline (same for all providers) +2. Transcription/diarization (provider-agnostic) +3. Database schema (add platform field, rest unchanged) +4. User consent flow (same UI/UX) + +## Platform-Specific Differences +| Feature | Whereby | Daily.co | +|---------|---------|----------| +| Meeting creation | REST API | REST API | +| Authentication | API key header | Bearer token | +| Recording delivery | S3 upload | Download URL | +| Frontend SDK | Web component | iframe/React SDK | +| Webhook signature | HMAC + timestamp | HMAC only | +| Room expiration | Automatic | Manual or via exp field | +``` + +### Step 1.3: Define Standard Data Models + +**Create `server/reflector/video_platforms/models.py`:** + +```python +from datetime import datetime +from typing import Literal, Optional +from pydantic import BaseModel, Field + + +Platform = Literal["whereby", "daily"] + + +class MeetingData(BaseModel): + """Standardized meeting data returned by all providers.""" + + platform: Platform + meeting_id: str = Field(description="Platform-specific meeting identifier") + room_url: str = Field(description="URL for participants to join") + host_room_url: str = Field(description="URL for hosts (may be same as room_url)") + room_name: str = Field(description="Human-readable room name") + start_date: Optional[datetime] = None + end_date: datetime + + class Config: + json_schema_extra = { + "example": { + "platform": "whereby", + "meeting_id": "12345678", + "room_url": "https://subdomain.whereby.com/room-20251008120000", + "host_room_url": "https://subdomain.whereby.com/room-20251008120000?roomKey=abc123", + "room_name": "room-20251008120000", + "end_date": "2025-10-08T14:00:00Z" + } + } + + +class VideoPlatformConfig(BaseModel): + """Platform-agnostic configuration model.""" + + api_key: str + webhook_secret: Optional[str] = None + subdomain: Optional[str] = None # Whereby/Daily subdomain + s3_bucket: Optional[str] = None + s3_region: str = "us-west-2" + # Whereby uses access keys, Daily uses IAM role + aws_access_key: Optional[str] = None + aws_secret_key: Optional[str] = None + aws_role_arn: Optional[str] = None + + +class RecordingType: + """Recording type constants.""" + NONE = "none" + LOCAL = "local" + CLOUD = "cloud" +``` + +### Deliverables +- [ ] Whereby integration analysis document +- [ ] Abstraction requirements document +- [ ] Standard data models in `models.py` +- [ ] List of files requiring modification + +--- + +## Phase 2: Platform Abstraction Layer (4-5 hours) + +### Objective +Create a clean abstraction layer without breaking existing Whereby functionality. + +### Step 2.1: Create Base Abstraction + +**File: `server/reflector/video_platforms/__init__.py`** + +```python +"""Video platform abstraction layer.""" + +from .base import VideoPlatformClient +from .models import Platform, MeetingData, VideoPlatformConfig +from .factory import create_platform_client, get_platform_config +from .registry import register_platform, get_platform_client_class + +__all__ = [ + "VideoPlatformClient", + "Platform", + "MeetingData", + "VideoPlatformConfig", + "create_platform_client", + "get_platform_config", + "register_platform", + "get_platform_client_class", +] +``` + +**File: `server/reflector/video_platforms/base.py`** + +```python +"""Abstract base class for video platform clients.""" + +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional +from datetime import datetime + +from .models import MeetingData, Platform, VideoPlatformConfig + +# Import Room with TYPE_CHECKING to avoid circular imports +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from reflector.db.rooms import Room + + +class VideoPlatformClient(ABC): + """ + Abstract base class for video platform integrations. + + All video platform providers (Whereby, Daily.co, etc.) must implement + this interface to ensure consistent behavior across the application. + + Design Principles: + - Methods should be platform-agnostic in their contracts + - Return standardized data models (MeetingData) + - Raise HTTPException for errors (FastAPI integration) + - Use async/await for all I/O operations + """ + + PLATFORM_NAME: Platform # Must be set by subclasses + + def __init__(self, config: VideoPlatformConfig): + """ + Initialize the platform client with configuration. + + Args: + config: Platform configuration with API keys, webhooks, etc. + """ + self.config = config + + @abstractmethod + async def create_meeting( + self, + room_name_prefix: str, + end_date: datetime, + room: "Room", + ) -> MeetingData: + """ + Create a new meeting room on the platform. + + Args: + room_name_prefix: Prefix for generating unique room name + end_date: When the meeting should expire + room: Room database model with configuration + + Returns: + MeetingData with platform-specific meeting details + + Raises: + HTTPException: On API errors or validation failures + + Implementation Notes: + - Generate room name as: {prefix}-YYYYMMDDHHMMSS + - Configure recording based on room.recording_type + - Set privacy based on room.is_locked + - Use room.room_size if platform supports capacity limits + """ + pass + + @abstractmethod + async def get_room_sessions(self, room_name: str) -> Dict[str, Any]: + """ + Get current session information for a room. + + Args: + room_name: The room identifier + + Returns: + Platform-specific session data + + Raises: + HTTPException: If room not found or API error + """ + pass + + @abstractmethod + async def delete_room(self, room_name: str) -> bool: + """ + Delete a room from the platform. + + Args: + room_name: The room identifier + + Returns: + True if deleted successfully or already doesn't exist + + Raises: + HTTPException: On API errors (except 404) + + Implementation Notes: + - Some platforms (Whereby) auto-expire rooms and don't support deletion + - Return True for 404 (idempotent operation) + """ + pass + + @abstractmethod + async def upload_logo(self, room_name: str, logo_path: str) -> bool: + """ + Upload a custom logo for a room (if supported). + + Args: + room_name: The room identifier + logo_path: Path to logo file + + Returns: + True if uploaded successfully + + Implementation Notes: + - Not all platforms support per-room logos + - Return True immediately if not supported (graceful degradation) + """ + pass + + @abstractmethod + def verify_webhook_signature( + self, + body: bytes, + signature: str, + timestamp: Optional[str] = None, + ) -> bool: + """ + Verify webhook request authenticity using HMAC signature. + + Args: + body: Raw request body bytes + signature: Signature from request header + timestamp: Optional timestamp for replay attack prevention + + Returns: + True if signature is valid + + Implementation Notes: + - Use constant-time comparison (hmac.compare_digest) + - Whereby: signature format is "t={timestamp},v1={sig}" + - Daily.co: signature is simple HMAC hex digest + - Implement timestamp freshness check if platform supports it + """ + pass +``` + +### Step 2.2: Create Platform Registry + +**File: `server/reflector/video_platforms/registry.py`** + +```python +"""Platform registration and discovery system.""" + +from typing import Dict, Type +from .base import VideoPlatformClient +from .models import Platform + + +# Global registry of available platform clients +_PLATFORMS: Dict[Platform, Type[VideoPlatformClient]] = {} + + +def register_platform( + platform_name: Platform, + client_class: Type[VideoPlatformClient], +) -> None: + """ + Register a video platform client implementation. + + Args: + platform_name: Unique platform identifier ("whereby", "daily") + client_class: Client class implementing VideoPlatformClient + + Example: + register_platform("whereby", WherebyClient) + """ + if platform_name in _PLATFORMS: + raise ValueError(f"Platform '{platform_name}' already registered") + + # Validate that the class implements the interface + if not issubclass(client_class, VideoPlatformClient): + raise TypeError( + f"Client class must inherit from VideoPlatformClient, " + f"got {client_class}" + ) + + _PLATFORMS[platform_name] = client_class + + +def get_platform_client_class(platform: Platform) -> Type[VideoPlatformClient]: + """ + Retrieve a registered platform client class. + + Args: + platform: Platform identifier + + Returns: + Client class for the specified platform + + Raises: + ValueError: If platform not registered + """ + if platform not in _PLATFORMS: + available = ", ".join(_PLATFORMS.keys()) + raise ValueError( + f"Unknown platform '{platform}'. " + f"Available platforms: {available}" + ) + + return _PLATFORMS[platform] + + +def get_available_platforms() -> list[Platform]: + """Get list of all registered platforms.""" + return list(_PLATFORMS.keys()) +``` + +### Step 2.3: Create Platform Factory + +**File: `server/reflector/video_platforms/factory.py`** + +```python +"""Factory functions for creating platform clients.""" + +from typing import Optional +from reflector import settings +from .base import VideoPlatformClient +from .models import Platform, VideoPlatformConfig +from .registry import get_platform_client_class + + +def get_platform_config(platform: Platform) -> VideoPlatformConfig: + """ + Build platform-specific configuration from settings. + + Args: + platform: Platform identifier + + Returns: + VideoPlatformConfig with platform-specific values + + Raises: + ValueError: If required settings are missing + """ + if platform == "whereby": + if not settings.WHEREBY_API_KEY: + raise ValueError("WHEREBY_API_KEY is required for Whereby platform") + + return VideoPlatformConfig( + api_key=settings.WHEREBY_API_KEY, + webhook_secret=settings.WHEREBY_WEBHOOK_SECRET, + subdomain=None, # Whereby doesn't use subdomains + s3_bucket=settings.AWS_WHEREBY_S3_BUCKET, + s3_region=settings.AWS_S3_REGION or "us-west-2", + aws_access_key=settings.AWS_WHEREBY_ACCESS_KEY_ID, + aws_secret_key=settings.AWS_WHEREBY_ACCESS_KEY_SECRET, + ) + + elif platform == "daily": + if not settings.DAILY_API_KEY: + raise ValueError("DAILY_API_KEY is required for Daily.co platform") + + return VideoPlatformConfig( + api_key=settings.DAILY_API_KEY, + webhook_secret=settings.DAILY_WEBHOOK_SECRET, + subdomain=settings.DAILY_SUBDOMAIN, + s3_bucket=settings.AWS_DAILY_S3_BUCKET, + s3_region=settings.AWS_DAILY_S3_REGION or "us-west-2", + aws_role_arn=settings.AWS_DAILY_ROLE_ARN, + ) + + else: + raise ValueError(f"Unknown platform: {platform}") + + +def create_platform_client(platform: Platform) -> VideoPlatformClient: + """ + Create and configure a platform client instance. + + Args: + platform: Platform identifier + + Returns: + Configured client instance + + Example: + client = create_platform_client("whereby") + meeting = await client.create_meeting(...) + """ + config = get_platform_config(platform) + client_class = get_platform_client_class(platform) + return client_class(config) + + +def get_platform_for_room(room_id: Optional[str] = None) -> Platform: + """ + Determine which platform to use for a room. + + This implements the platform selection strategy using feature flags. + + Args: + room_id: Optional room ID for room-specific overrides + + Returns: + Platform to use + + Platform Selection Logic: + 1. If DAILY_MIGRATION_ENABLED=False → always use "whereby" + 2. If room_id in DAILY_MIGRATION_ROOM_IDS → use "daily" + 3. Otherwise → use DEFAULT_VIDEO_PLATFORM + + Example Environment Variables: + DAILY_MIGRATION_ENABLED=true + DAILY_MIGRATION_ROOM_IDS=["room-abc", "room-xyz"] + DEFAULT_VIDEO_PLATFORM=whereby + """ + # If Daily migration is disabled, always use Whereby + if not settings.DAILY_MIGRATION_ENABLED: + return "whereby" + + # If specific room is in migration list, use Daily + if room_id and room_id in settings.DAILY_MIGRATION_ROOM_IDS: + return "daily" + + # Otherwise use the configured default + return settings.DEFAULT_VIDEO_PLATFORM +``` + +### Step 2.4: Create Mock Implementation for Testing + +**File: `server/reflector/video_platforms/mock.py`** + +```python +"""Mock video platform client for testing.""" + +import hmac +from datetime import datetime, timedelta +from typing import Any, Dict, Optional +from hashlib import sha256 + +from .base import VideoPlatformClient +from .models import MeetingData, Platform + +# Import with TYPE_CHECKING to avoid circular imports +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from reflector.db.rooms import Room + + +class MockClient(VideoPlatformClient): + """ + Mock video platform client for unit testing. + + This client simulates a video platform without making real API calls. + Useful for testing business logic without external dependencies. + """ + + PLATFORM_NAME: Platform = "whereby" # Pretend to be Whereby for backward compat + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._rooms: Dict[str, Dict[str, Any]] = {} + self._participants: Dict[str, int] = {} + + async def create_meeting( + self, + room_name_prefix: str, + end_date: datetime, + room: "Room", + ) -> MeetingData: + """Create a mock meeting.""" + room_name = f"{room_name_prefix}-{datetime.now().strftime('%Y%m%d%H%M%S')}" + + self._rooms[room_name] = { + "name": room_name, + "end_date": end_date, + "room": room, + } + self._participants[room_name] = 0 + + return MeetingData( + platform=self.PLATFORM_NAME, + meeting_id=f"mock-{room_name}", + room_url=f"https://mock.example.com/{room_name}", + host_room_url=f"https://mock.example.com/{room_name}?host=true", + room_name=room_name, + end_date=end_date, + ) + + async def get_room_sessions(self, room_name: str) -> Dict[str, Any]: + """Get mock room session data.""" + if room_name not in self._rooms: + raise ValueError(f"Room {room_name} not found") + + return { + "room_name": room_name, + "participants": self._participants.get(room_name, 0), + "created": self._rooms[room_name].get("created", datetime.now()), + } + + async def delete_room(self, room_name: str) -> bool: + """Delete mock room.""" + if room_name in self._rooms: + del self._rooms[room_name] + del self._participants[room_name] + return True + + async def upload_logo(self, room_name: str, logo_path: str) -> bool: + """Mock logo upload (always succeeds).""" + return True + + def verify_webhook_signature( + self, + body: bytes, + signature: str, + timestamp: Optional[str] = None, + ) -> bool: + """Mock signature verification (accepts 'valid' as signature).""" + return signature == "valid" + + # Test helper methods + def add_participant(self, room_name: str) -> None: + """Add a participant to a room (test helper).""" + if room_name in self._participants: + self._participants[room_name] += 1 + + def remove_participant(self, room_name: str) -> None: + """Remove a participant from a room (test helper).""" + if room_name in self._participants and self._participants[room_name] > 0: + self._participants[room_name] -= 1 + + def clear_data(self) -> None: + """Clear all mock data (test helper).""" + self._rooms.clear() + self._participants.clear() +``` + +### Step 2.5: Implement Whereby Client Wrapper + +**File: `server/reflector/video_platforms/whereby.py`** + +```python +"""Whereby platform client implementation.""" + +import re +import hmac +import json +from datetime import datetime +from hashlib import sha256 +from typing import Any, Dict, Optional + +import httpx +from fastapi import HTTPException + +from .base import VideoPlatformClient +from .models import MeetingData, Platform + +# Import with TYPE_CHECKING to avoid circular imports +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from reflector.db.rooms import Room + + +class WherebyClient(VideoPlatformClient): + """ + Whereby video platform client. + + Wraps the existing Whereby API integration into the platform abstraction. + + API Documentation: https://docs.whereby.com/ + """ + + PLATFORM_NAME: Platform = "whereby" + BASE_URL = "https://api.whereby.dev/v1" + TIMEOUT = 10 + SIGNATURE_MAX_AGE = 60 # seconds + + def __init__(self, config): + super().__init__(config) + self.headers = { + "Authorization": f"Bearer {config.api_key}", + "Content-Type": "application/json", + } + + async def create_meeting( + self, + room_name_prefix: str, + end_date: datetime, + room: "Room", + ) -> MeetingData: + """ + Create a Whereby meeting room. + + See: https://docs.whereby.com/reference/whereby-rest-api-reference#create-meeting + """ + room_name = f"{room_name_prefix}-{datetime.now().strftime('%Y%m%d%H%M%S')}" + + # Build request payload + data = { + "endDate": end_date.isoformat(), + "fields": ["hostRoomUrl"], + "roomNamePrefix": f"/{room_name}", + } + + # Configure room mode based on lock status + if room.is_locked: + data["roomMode"] = "normal" + else: + data["roomMode"] = "group" + + # Configure recording if enabled + if room.recording_type == "cloud" and self.config.s3_bucket: + data["recording"] = { + "type": "cloud", + "destination": { + "provider": "s3", + "config": { + "bucket": self.config.s3_bucket, + "region": self.config.s3_region, + "accessKeyId": self.config.aws_access_key, + "secretAccessKey": self.config.aws_secret_key, + } + } + } + + # Make API request + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.BASE_URL}/meetings", + headers=self.headers, + json=data, + timeout=self.TIMEOUT, + ) + response.raise_for_status() + result = response.json() + + # Transform to standard format + return MeetingData( + platform=self.PLATFORM_NAME, + meeting_id=result["meetingId"], + room_url=result["roomUrl"], + host_room_url=result.get("hostRoomUrl", result["roomUrl"]), + room_name=room_name, + end_date=end_date, + ) + + async def get_room_sessions(self, room_name: str) -> Dict[str, Any]: + """Get Whereby room session data.""" + async with httpx.AsyncClient() as client: + response = await client.get( + f"{self.BASE_URL}/meetings/{room_name}", + headers=self.headers, + timeout=self.TIMEOUT, + ) + response.raise_for_status() + return response.json() + + async def delete_room(self, room_name: str) -> bool: + """ + Whereby rooms auto-expire, so deletion is a no-op. + + Returns True to maintain interface compatibility. + """ + return True + + async def upload_logo(self, room_name: str, logo_path: str) -> bool: + """ + Upload custom logo to Whereby room. + + Note: This requires reading the logo file and making a multipart request. + Implementation depends on logo storage strategy. + """ + # TODO: Implement logo upload if needed + # For now, return True (feature not critical) + return True + + def verify_webhook_signature( + self, + body: bytes, + signature: str, + timestamp: Optional[str] = None, + ) -> bool: + """ + Verify Whereby webhook signature. + + Whereby signature format: "t={timestamp},v1={signature}" + Algorithm: HMAC-SHA256(webhook_secret, timestamp + "." + body) + + See: https://docs.whereby.com/reference/whereby-rest-api-reference#webhook-signatures + """ + if not self.config.webhook_secret: + raise ValueError("webhook_secret is required for signature verification") + + # Parse signature format: t={timestamp},v1={signature} + matches = re.match(r"t=(.*),v1=(.*)", signature) + if not matches: + return False + + sig_timestamp, sig_hash = matches.groups() + + # Check timestamp freshness (prevent replay attacks) + try: + ts = int(sig_timestamp) + now = int(datetime.now().timestamp()) + if abs(now - ts) > self.SIGNATURE_MAX_AGE: + return False + except (ValueError, TypeError): + return False + + # Compute expected signature + message = f"{sig_timestamp}.{body.decode('utf-8')}" + expected_sig = hmac.new( + self.config.webhook_secret.encode(), + message.encode(), + sha256 + ).hexdigest() + + # Constant-time comparison + return hmac.compare_digest(expected_sig, sig_hash) +``` + +### Step 2.6: Register Whereby Client + +**Add to `server/reflector/video_platforms/__init__.py`:** + +```python +# Auto-register built-in platforms +from .whereby import WherebyClient +from .mock import MockClient + +register_platform("whereby", WherebyClient) +``` + +### Step 2.7: Update Settings + +**File: `server/reflector/settings.py`** + +Add Daily.co settings and feature flags: + +```python +# Existing Whereby settings (already present) +WHEREBY_API_URL: str = "https://api.whereby.dev/v1" +WHEREBY_API_KEY: str | None = None +WHEREBY_WEBHOOK_SECRET: str | None = None +AWS_WHEREBY_S3_BUCKET: str | None = None +AWS_WHEREBY_ACCESS_KEY_ID: str | None = None +AWS_WHEREBY_ACCESS_KEY_SECRET: str | None = None + +# NEW: Daily.co API Integration +DAILY_API_KEY: str | None = None +DAILY_WEBHOOK_SECRET: str | None = None +DAILY_SUBDOMAIN: str | None = None +AWS_DAILY_S3_BUCKET: str | None = None +AWS_DAILY_S3_REGION: str = "us-west-2" +AWS_DAILY_ROLE_ARN: str | None = None + +# NEW: Platform Migration Feature Flags +DAILY_MIGRATION_ENABLED: bool = False # Conservative default +DAILY_MIGRATION_ROOM_IDS: list[str] = [] # Specific rooms for gradual rollout +DEFAULT_VIDEO_PLATFORM: Literal["whereby", "daily"] = "whereby" # Default to Whereby +``` + +### Step 2.8: Update Database Schema + +**Create migration: `server/migrations/versions/YYYYMMDDHHMMSS_add_platform_support.py`** + +```bash +cd server +uv run alembic revision -m "add_platform_support" +``` + +**Migration content:** + +```python +"""add_platform_support + +Adds platform field to room and meeting tables to support multi-provider architecture. + +Revision ID: +Revises: +Create Date: +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers +revision = '' +down_revision = '' +branch_labels = None +depends_on = None + + +def upgrade(): + """Add platform field with default 'whereby' for backward compatibility.""" + + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "platform", + sa.String(), + nullable=False, + server_default="whereby", + ) + ) + + with op.batch_alter_table("meeting", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "platform", + sa.String(), + nullable=False, + server_default="whereby", + ) + ) + + +def downgrade(): + """Remove platform field.""" + + with op.batch_alter_table("meeting", schema=None) as batch_op: + batch_op.drop_column("platform") + + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.drop_column("platform") +``` + +**Update models:** + +**File: `server/reflector/db/rooms.py`** + +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from reflector.video_platforms.models import Platform + +class Room: + # ... existing fields ... + + # NEW: Platform field + platform: "Platform" = sqlalchemy.Column( + sqlalchemy.String, + nullable=False, + server_default="whereby", + ) +``` + +**File: `server/reflector/db/meetings.py`** + +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from reflector.video_platforms.models import Platform + +class Meeting: + # ... existing fields ... + + # NEW: Platform field + platform: "Platform" = sqlalchemy.Column( + sqlalchemy.String, + nullable=False, + server_default="whereby", + ) +``` + +### Step 2.9: Refactor Room/Meeting Creation + +**File: `server/reflector/views/rooms.py`** + +Replace direct Whereby API calls with platform abstraction: + +```python +from reflector.video_platforms import ( + create_platform_client, + get_platform_for_room, +) + +# OLD CODE (remove): +# from reflector import whereby +# meeting_data = whereby.create_meeting(...) + +# NEW CODE: +@router.post("/rooms", response_model=RoomResponse) +async def create_room(room_data: RoomCreate): + """Create a new room.""" + + # Determine platform for new room + platform = get_platform_for_room() + + # Create room in database + room = Room( + name=room_data.name, + is_locked=room_data.is_locked, + recording_type=room_data.recording_type, + platform=platform, # NEW: Store platform + # ... other fields ... + ) + await room.save() + + return RoomResponse.from_orm(room) + + +@router.post("/rooms/{room_name}/meeting", response_model=MeetingResponse) +async def create_meeting(room_name: str, meeting_data: MeetingCreate): + """Create a new meeting in a room.""" + + # Get room + room = await Room.get_by_name(room_name) + if not room: + raise HTTPException(status_code=404, detail="Room not found") + + # Use platform abstraction instead of direct Whereby calls + platform = get_platform_for_room(room.id) # Respects feature flags + client = create_platform_client(platform) + + # Create meeting via platform client + meeting_data = await client.create_meeting( + room_name_prefix=room.name, + end_date=meeting_data.end_date, + room=room, + ) + + # Create database record + meeting = Meeting( + room_id=room.id, + platform=meeting_data.platform, # NEW: Store platform + meeting_id=meeting_data.meeting_id, + room_url=meeting_data.room_url, + host_room_url=meeting_data.host_room_url, + # ... other fields ... + ) + await meeting.save() + + # Upload logo if configured (platform handles graceful degradation) + if room.logo_path: + await client.upload_logo(meeting_data.room_name, room.logo_path) + + return MeetingResponse.from_orm(meeting) +``` + +### Deliverables +- [ ] Platform abstraction layer (`base.py`, `registry.py`, `factory.py`, `models.py`) +- [ ] Whereby client wrapper (`whereby.py`) +- [ ] Mock client for testing (`mock.py`) +- [ ] Database migration for platform field +- [ ] Updated room/meeting models +- [ ] Refactored room creation logic +- [ ] Updated settings with feature flags + +--- + +## Phase 3: Daily.co Implementation (4-5 hours) + +### Objective +Implement Daily.co provider with feature parity to Whereby. + +### Step 3.1: Implement Daily.co Client + +**File: `server/reflector/video_platforms/daily.py`** + +```python +"""Daily.co platform client implementation.""" + +import hmac +from datetime import datetime +from hashlib import sha256 +from typing import Any, Dict, Optional + +import httpx +from fastapi import HTTPException + +from .base import VideoPlatformClient +from .models import MeetingData, Platform + +# Import with TYPE_CHECKING to avoid circular imports +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from reflector.db.rooms import Room + + +class DailyClient(VideoPlatformClient): + """ + Daily.co video platform client. + + API Documentation: https://docs.daily.co/reference/rest-api + """ + + PLATFORM_NAME: Platform = "daily" + BASE_URL = "https://api.daily.co/v1" + TIMEOUT = 10 + + def __init__(self, config): + super().__init__(config) + self.headers = { + "Authorization": f"Bearer {config.api_key}", + "Content-Type": "application/json", + } + + async def create_meeting( + self, + room_name_prefix: str, + end_date: datetime, + room: "Room", + ) -> MeetingData: + """ + Create a Daily.co room. + + See: https://docs.daily.co/reference/rest-api/rooms/create-room + """ + room_name = f"{room_name_prefix}-{datetime.now().strftime('%Y%m%d%H%M%S')}" + + # Build request payload + data = { + "name": room_name, + "privacy": "private" if room.is_locked else "public", + "properties": { + "exp": int(end_date.timestamp()), + "enable_chat": True, + "enable_screenshare": True, + "start_video_off": False, + "start_audio_off": False, + } + } + + # Configure recording if enabled + if room.recording_type == "cloud": + data["properties"]["enable_recording"] = "cloud" + + # Configure S3 recording destination if bucket configured + if self.config.s3_bucket and self.config.aws_role_arn: + data["properties"]["recordings_bucket"] = { + "bucket_name": self.config.s3_bucket, + "bucket_region": self.config.s3_region, + "assume_role_arn": self.config.aws_role_arn, + "allow_api_access": True, + } + elif room.recording_type == "local": + data["properties"]["enable_recording"] = "local" + else: + data["properties"]["enable_recording"] = False + + # Make API request + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.BASE_URL}/rooms", + headers=self.headers, + json=data, + timeout=self.TIMEOUT, + ) + response.raise_for_status() + result = response.json() + + # Build room URL + room_url = result["url"] + + # Daily.co doesn't have separate host URLs + host_room_url = room_url + + # Transform to standard format + return MeetingData( + platform=self.PLATFORM_NAME, + meeting_id=result["id"], # Daily.co room ID + room_url=room_url, + host_room_url=host_room_url, + room_name=result["name"], + end_date=end_date, + ) + + async def get_room_sessions(self, room_name: str) -> Dict[str, Any]: + """ + Get Daily.co room information. + + See: https://docs.daily.co/reference/rest-api/rooms/get-room-info + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{self.BASE_URL}/rooms/{room_name}", + headers=self.headers, + timeout=self.TIMEOUT, + ) + response.raise_for_status() + return response.json() + + async def get_room_presence(self, room_name: str) -> Dict[str, Any]: + """ + Get real-time participant presence (Daily.co-specific feature). + + See: https://docs.daily.co/reference/rest-api/rooms/get-room-presence + + Note: This method is NOT in the base interface since it's platform-specific. + Only call this if you know you're using Daily.co. + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{self.BASE_URL}/rooms/{room_name}/presence", + headers=self.headers, + timeout=self.TIMEOUT, + ) + response.raise_for_status() + return response.json() + + async def delete_room(self, room_name: str) -> bool: + """ + Delete a Daily.co room. + + See: https://docs.daily.co/reference/rest-api/rooms/delete-room + """ + async with httpx.AsyncClient() as client: + response = await client.delete( + f"{self.BASE_URL}/rooms/{room_name}", + headers=self.headers, + timeout=self.TIMEOUT, + ) + + # Accept both 200 (deleted) and 404 (already gone) as success + if response.status_code in (200, 404): + return True + + response.raise_for_status() + return False + + async def upload_logo(self, room_name: str, logo_path: str) -> bool: + """ + Daily.co doesn't support per-room logos. + + Return True for interface compatibility (graceful degradation). + """ + return True + + def verify_webhook_signature( + self, + body: bytes, + signature: str, + timestamp: Optional[str] = None, + ) -> bool: + """ + Verify Daily.co webhook signature. + + Daily.co signature format: Simple HMAC-SHA256 hex digest + Header: X-Daily-Signature + Algorithm: HMAC-SHA256(webhook_secret, body) + + See: https://docs.daily.co/reference/rest-api/webhooks#webhook-signatures + """ + if not self.config.webhook_secret: + raise ValueError("webhook_secret is required for signature verification") + + # Compute expected signature + expected_sig = hmac.new( + self.config.webhook_secret.encode(), + body, + sha256 + ).hexdigest() + + # Constant-time comparison + return hmac.compare_digest(expected_sig, signature) +``` + +### Step 3.2: Register Daily.co Client + +**Update `server/reflector/video_platforms/__init__.py`:** + +```python +# Auto-register built-in platforms +from .whereby import WherebyClient +from .daily import DailyClient +from .mock import MockClient + +register_platform("whereby", WherebyClient) +register_platform("daily", DailyClient) +``` + +### Step 3.3: Create Daily.co Webhook Handler + +**File: `server/reflector/views/daily.py`** + +```python +"""Daily.co webhook endpoint.""" + +from fastapi import APIRouter, HTTPException, Header, Request +from pydantic import BaseModel, Field +from typing import Literal, Optional +from datetime import datetime + +from reflector import settings +from reflector.logger import logger +from reflector.db.meetings import Meeting +from reflector.video_platforms import create_platform_client + + +router = APIRouter() + + +# Webhook event models +class DailyWebhookEvent(BaseModel): + """Base Daily.co webhook event.""" + type: str + payload: dict + id: str + timestamp: datetime + + +class ParticipantPayload(BaseModel): + """Participant event payload.""" + room: str + participant_id: str + user_name: Optional[str] = None + + +class RecordingPayload(BaseModel): + """Recording event payload.""" + room: str + recording_id: str + download_url: Optional[str] = None + error: Optional[str] = None + + +@router.post("/webhook") +async def daily_webhook( + request: Request, + x_daily_signature: str = Header(None), +): + """ + Handle Daily.co webhook events. + + Supported events: + - participant.joined - Update participant count + - participant.left - Update participant count + - recording.started - Log recording start + - recording.ready-to-download - Trigger processing + - recording.error - Log recording errors + + See: https://docs.daily.co/reference/rest-api/webhooks + """ + # Get raw body for signature verification + body = await request.body() + + # Verify signature + if not x_daily_signature: + raise HTTPException(status_code=400, detail="Missing X-Daily-Signature header") + + client = create_platform_client("daily") + if not client.verify_webhook_signature(body, x_daily_signature): + logger.warning("Daily.co webhook signature verification failed") + raise HTTPException(status_code=401, detail="Invalid signature") + + # Parse event + try: + event = DailyWebhookEvent.parse_raw(body) + except Exception as e: + logger.error(f"Failed to parse Daily.co webhook: {e}") + raise HTTPException(status_code=400, detail="Invalid event format") + + logger.info(f"Daily.co webhook event: {event.type} (room: {event.payload.get('room')})") + + # Handle event by type + if event.type == "participant.joined": + await handle_participant_joined(event) + + elif event.type == "participant.left": + await handle_participant_left(event) + + elif event.type == "recording.started": + await handle_recording_started(event) + + elif event.type == "recording.ready-to-download": + await handle_recording_ready(event) + + elif event.type == "recording.error": + await handle_recording_error(event) + + else: + logger.warning(f"Unhandled Daily.co event type: {event.type}") + + return {"status": "ok"} + + +async def handle_participant_joined(event: DailyWebhookEvent): + """Handle participant joining a room.""" + room_name = event.payload.get("room") + if not room_name: + return + + # Find active meeting for this room + meeting = await Meeting.get_active_by_room_name(room_name) + if not meeting: + logger.warning(f"No active meeting found for room: {room_name}") + return + + # Increment participant count + meeting.num_clients = (meeting.num_clients or 0) + 1 + await meeting.save() + + logger.info(f"Participant joined {room_name}, count: {meeting.num_clients}") + + +async def handle_participant_left(event: DailyWebhookEvent): + """Handle participant leaving a room.""" + room_name = event.payload.get("room") + if not room_name: + return + + # Find active meeting for this room + meeting = await Meeting.get_active_by_room_name(room_name) + if not meeting: + return + + # Decrement participant count (don't go below 0) + meeting.num_clients = max(0, (meeting.num_clients or 1) - 1) + await meeting.save() + + logger.info(f"Participant left {room_name}, count: {meeting.num_clients}") + + +async def handle_recording_started(event: DailyWebhookEvent): + """Handle recording start.""" + room_name = event.payload.get("room") + recording_id = event.payload.get("recording_id") + + logger.info(f"Recording started for room {room_name}: {recording_id}") + + +async def handle_recording_ready(event: DailyWebhookEvent): + """Handle recording ready for download.""" + room_name = event.payload.get("room") + recording_id = event.payload.get("recording_id") + download_url = event.payload.get("download_url") + + if not download_url: + logger.error(f"Recording ready but no download URL: {recording_id}") + return + + # Find meeting for this room + meeting = await Meeting.get_by_room_name(room_name) + if not meeting: + logger.error(f"No meeting found for room: {room_name}") + return + + logger.info(f"Recording ready: {recording_id}, triggering processing") + + # Trigger background processing + from reflector.worker.process import process_recording_from_url + process_recording_from_url.delay( + recording_url=download_url, + meeting_id=str(meeting.id), + recording_id=recording_id, + ) + + +async def handle_recording_error(event: DailyWebhookEvent): + """Handle recording errors.""" + room_name = event.payload.get("room") + recording_id = event.payload.get("recording_id") + error = event.payload.get("error") + + logger.error( + f"Recording error for room {room_name}: {error}", + extra={"recording_id": recording_id} + ) +``` + +**Register router in `server/reflector/app.py`:** + +```python +from reflector.views import daily + +app.include_router(daily.router, prefix="/v1/daily", tags=["daily"]) +``` + +### Step 3.4: Create Recording Processing Task + +**Update `server/reflector/worker/process.py`:** + +```python +from celery import shared_task +import httpx +from pathlib import Path +import tempfile + +from reflector.db.recordings import Recording +from reflector.db.meetings import Meeting +from reflector.logger import logger + + +@shared_task +@asynctask +async def process_recording_from_url( + recording_url: str, + meeting_id: str, + recording_id: str, +): + """ + Download and process a recording from a URL (Daily.co). + + This task is triggered by Daily.co webhooks when a recording is ready. + + Args: + recording_url: HTTPS URL to download recording from + meeting_id: Database ID of the meeting + recording_id: Platform-specific recording identifier + """ + logger.info(f"Processing recording from URL: {recording_id}") + + # Get meeting + meeting = await Meeting.get(meeting_id) + if not meeting: + logger.error(f"Meeting not found: {meeting_id}") + return + + # Download recording to temporary file + try: + async with httpx.AsyncClient() as client: + response = await client.get( + recording_url, + timeout=300, # 5 minutes for large files + ) + response.raise_for_status() + + # Save to temporary file + with tempfile.NamedTemporaryFile( + suffix=".mp4", + delete=False, + ) as tmp_file: + tmp_file.write(response.content) + local_path = tmp_file.name + + logger.info(f"Downloaded recording to: {local_path}") + + except Exception as e: + logger.error(f"Failed to download recording: {e}") + return + + try: + # Validate audio stream exists + import ffmpeg + probe = ffmpeg.probe(local_path) + audio_streams = [ + s for s in probe["streams"] + if s["codec_type"] == "audio" + ] + + if not audio_streams: + logger.error(f"No audio stream in recording: {recording_id}") + return + + # Create recording record + recording = Recording( + meeting_id=meeting_id, + bucket="daily-recordings", # Logical bucket name + object_key=recording_id, # Store Daily.co recording ID + local_path=local_path, + status="downloaded", + ) + await recording.save() + + logger.info(f"Created recording record: {recording.id}") + + # Trigger main processing pipeline + from reflector.worker.pipeline import task_pipeline_process + task_pipeline_process.delay(transcript_id=str(recording.transcript_id)) + + except Exception as e: + logger.error(f"Failed to process recording: {e}") + # Clean up temporary file on error + Path(local_path).unlink(missing_ok=True) + raise +``` + +### Step 3.5: Create Frontend Components + +**File: `www/app/[roomName]/components/RoomContainer.tsx`** + +```typescript +'use client' + +import { useEffect, useState } from 'react' +import WherebyRoom from './WherebyRoom' +import DailyRoom from './DailyRoom' +import { Meeting } from '@/app/api/types.gen' + +interface RoomContainerProps { + meeting: Meeting +} + +export default function RoomContainer({ meeting }: RoomContainerProps) { + // Determine platform from meeting response + const platform = meeting.platform || 'whereby' // Default for backward compat + + // Route to appropriate platform component + if (platform === 'daily') { + return + } + + // Default to Whereby + return +} +``` + +**File: `www/app/[roomName]/components/DailyRoom.tsx`** + +```typescript +'use client' + +import { useEffect, useRef, useState } from 'react' +import { useRouter } from 'next/navigation' +import DailyIframe, { DailyCall } from '@daily-co/daily-js' +import { Meeting } from '@/app/api/types.gen' +import { Box, Button, Text, useToast } from '@chakra-ui/react' +import { useSessionStatus } from '@/hooks/useSessionStatus' +import { api } from '@/app/api/client' + +interface DailyRoomProps { + meeting: Meeting +} + +export default function DailyRoom({ meeting }: DailyRoomProps) { + const router = useRouter() + const toast = useToast() + const containerRef = useRef(null) + const callFrameRef = useRef(null) + const [isLoading, setIsLoading] = useState(true) + const [showConsent, setShowConsent] = useState(false) + + const { sessionId } = useSessionStatus(meeting.id) + + useEffect(() => { + if (!containerRef.current) return + + // Check if recording requires consent + if (meeting.recording_type === 'cloud' && !sessionId) { + setShowConsent(true) + setIsLoading(false) + return + } + + // Create Daily.co iframe + const frame = DailyIframe.createFrame(containerRef.current, { + showLeaveButton: true, + showFullscreenButton: true, + iframeStyle: { + position: 'absolute', + width: '100%', + height: '100%', + border: 'none', + }, + }) + + callFrameRef.current = frame + + // Join meeting + frame.join({ url: meeting.room_url }) + .then(() => { + setIsLoading(false) + }) + .catch((error) => { + console.error('Failed to join Daily.co meeting:', error) + toast({ + title: 'Failed to join meeting', + description: error.message, + status: 'error', + duration: 5000, + }) + setIsLoading(false) + }) + + // Handle leave event + frame.on('left-meeting', () => { + router.push('/browse') + }) + + // Cleanup + return () => { + if (callFrameRef.current) { + callFrameRef.current.destroy() + callFrameRef.current = null + } + } + }, [meeting, sessionId, router, toast]) + + const handleConsent = async () => { + try { + await api.v1MeetingAudioConsent({ + path: { meeting_id: meeting.id }, + body: { consent: true }, + }) + setShowConsent(false) + // Trigger re-render to join meeting + window.location.reload() + } catch (error) { + toast({ + title: 'Failed to record consent', + description: 'Please try again', + status: 'error', + duration: 3000, + }) + } + } + + if (showConsent) { + return ( + + + Recording Consent Required + + + This meeting will be recorded and transcribed. Do you consent to + participate? + + + + + ) + } + + if (isLoading) { + return ( + + Loading meeting... + + ) + } + + return ( + + ) +} +``` + +**File: `www/app/[roomName]/components/WherebyRoom.tsx`** + +Extract existing room page logic into this component (no changes to functionality). + +**Update `www/app/[roomName]/page.tsx`:** + +```typescript +import RoomContainer from './components/RoomContainer' +import { api } from '@/app/api/client' + +export default async function RoomPage({ params }: { params: { roomName: string } }) { + const meeting = await api.v1GetActiveMeeting({ + path: { room_name: params.roomName } + }) + + return +} +``` + +### Step 3.6: Update Frontend Dependencies + +**Update `www/package.json`:** + +```bash +cd www +yarn add @daily-co/daily-js@^0.81.0 +``` + +### Step 3.7: Update Environment Configuration + +**Update `server/env.example`:** + +```bash +# Video Platform Configuration +# Whereby (existing provider) +WHEREBY_API_KEY=your-whereby-api-key +WHEREBY_WEBHOOK_SECRET=your-whereby-webhook-secret +AWS_WHEREBY_S3_BUCKET=your-whereby-bucket +AWS_WHEREBY_ACCESS_KEY_ID=your-aws-key +AWS_WHEREBY_ACCESS_KEY_SECRET=your-aws-secret + +# Daily.co (new provider) +DAILY_API_KEY=your-daily-api-key +DAILY_WEBHOOK_SECRET=your-daily-webhook-secret +DAILY_SUBDOMAIN=your-subdomain +AWS_DAILY_S3_BUCKET=your-daily-bucket +AWS_DAILY_S3_REGION=us-west-2 +AWS_DAILY_ROLE_ARN=arn:aws:iam::ACCOUNT:role/DailyRecording + +# Platform Selection +DAILY_MIGRATION_ENABLED=false # Enable Daily.co support +DAILY_MIGRATION_ROOM_IDS=[] # Specific rooms to use Daily +DEFAULT_VIDEO_PLATFORM=whereby # Default platform for new rooms +``` + +### Deliverables +- [ ] Daily.co client implementation +- [ ] Daily.co webhook handler +- [ ] Recording download task +- [ ] Frontend platform routing +- [ ] DailyRoom component +- [ ] WherebyRoom component extraction +- [ ] Updated environment configuration +- [ ] Frontend dependencies installed + +--- + +## Testing Strategy (3-4 hours) + +### Unit Tests + +**File: `server/tests/test_video_platforms.py`** + +```python +"""Unit tests for video platform abstraction.""" + +import pytest +from datetime import datetime, timedelta +from unittest.mock import Mock, patch, AsyncMock + +from reflector.video_platforms import ( + create_platform_client, + get_platform_config, + register_platform, + get_available_platforms, +) +from reflector.video_platforms.whereby import WherebyClient +from reflector.video_platforms.daily import DailyClient +from reflector.video_platforms.mock import MockClient + + +@pytest.fixture +def mock_room(): + """Create a mock room object.""" + room = Mock() + room.id = "room-123" + room.name = "test-room" + room.is_locked = False + room.recording_type = "cloud" + room.room_size = 10 + return room + + +def test_platform_registry(): + """Test platform registration and discovery.""" + platforms = get_available_platforms() + assert "whereby" in platforms + assert "daily" in platforms + + +def test_create_whereby_client(): + """Test Whereby client creation.""" + with patch("reflector.settings") as mock_settings: + mock_settings.WHEREBY_API_KEY = "test-key" + mock_settings.WHEREBY_WEBHOOK_SECRET = "test-secret" + + client = create_platform_client("whereby") + assert isinstance(client, WherebyClient) + assert client.PLATFORM_NAME == "whereby" + + +def test_create_daily_client(): + """Test Daily.co client creation.""" + with patch("reflector.settings") as mock_settings: + mock_settings.DAILY_API_KEY = "test-key" + mock_settings.DAILY_WEBHOOK_SECRET = "test-secret" + + client = create_platform_client("daily") + assert isinstance(client, DailyClient) + assert client.PLATFORM_NAME == "daily" + + +@pytest.mark.asyncio +async def test_whereby_signature_verification(): + """Test Whereby webhook signature verification.""" + config = VideoPlatformConfig( + api_key="test", + webhook_secret="test-secret", + ) + client = WherebyClient(config) + + # Generate valid signature + timestamp = str(int(datetime.now().timestamp())) + body = b'{"event": "test"}' + message = f"{timestamp}.{body.decode()}" + + import hmac + from hashlib import sha256 + signature = hmac.new( + b"test-secret", + message.encode(), + sha256 + ).hexdigest() + + sig_header = f"t={timestamp},v1={signature}" + + assert client.verify_webhook_signature(body, sig_header) + + +@pytest.mark.asyncio +async def test_daily_signature_verification(): + """Test Daily.co webhook signature verification.""" + config = VideoPlatformConfig( + api_key="test", + webhook_secret="test-secret", + ) + client = DailyClient(config) + + # Generate valid signature + body = b'{"event": "test"}' + + import hmac + from hashlib import sha256 + signature = hmac.new( + b"test-secret", + body, + sha256 + ).hexdigest() + + assert client.verify_webhook_signature(body, signature) + + +@pytest.mark.asyncio +async def test_mock_client_lifecycle(mock_room): + """Test mock client create/delete lifecycle.""" + config = VideoPlatformConfig(api_key="test") + client = MockClient(config) + + # Create meeting + end_date = datetime.now() + timedelta(hours=1) + meeting = await client.create_meeting("test", end_date, mock_room) + + assert meeting.platform == "whereby" # Mock pretends to be Whereby + assert "test-" in meeting.room_name + + # Get sessions + sessions = await client.get_room_sessions(meeting.room_name) + assert sessions["room_name"] == meeting.room_name + assert sessions["participants"] == 0 + + # Add participant + client.add_participant(meeting.room_name) + sessions = await client.get_room_sessions(meeting.room_name) + assert sessions["participants"] == 1 + + # Delete room + result = await client.delete_room(meeting.room_name) + assert result is True + + # Room should be gone + with pytest.raises(ValueError): + await client.get_room_sessions(meeting.room_name) +``` + +**File: `server/tests/test_daily_webhook.py`** + +```python +"""Integration tests for Daily.co webhook handler.""" + +import pytest +import hmac +from hashlib import sha256 +from datetime import datetime +from fastapi.testclient import TestClient + +from reflector.app import app +from reflector.db.meetings import Meeting + + +client = TestClient(app) + + +def create_webhook_signature(body: bytes, secret: str) -> str: + """Create Daily.co webhook signature.""" + return hmac.new(secret.encode(), body, sha256).hexdigest() + + +@pytest.mark.asyncio +async def test_participant_joined(mock_meeting): + """Test participant joined event.""" + webhook_secret = "test-secret" + + event_data = { + "type": "participant.joined", + "id": "evt_123", + "timestamp": datetime.now().isoformat(), + "payload": { + "room": mock_meeting.room_name, + "participant_id": "user_123", + } + } + + body = json.dumps(event_data).encode() + signature = create_webhook_signature(body, webhook_secret) + + with patch("reflector.settings.DAILY_WEBHOOK_SECRET", webhook_secret): + response = client.post( + "/v1/daily/webhook", + content=body, + headers={"X-Daily-Signature": signature} + ) + + assert response.status_code == 200 + + # Verify participant count increased + meeting = await Meeting.get(mock_meeting.id) + assert meeting.num_clients == 1 + + +@pytest.mark.asyncio +async def test_recording_ready(mock_meeting): + """Test recording ready event triggers processing.""" + webhook_secret = "test-secret" + + event_data = { + "type": "recording.ready-to-download", + "id": "evt_456", + "timestamp": datetime.now().isoformat(), + "payload": { + "room": mock_meeting.room_name, + "recording_id": "rec_789", + "download_url": "https://daily.co/recordings/rec_789.mp4", + } + } + + body = json.dumps(event_data).encode() + signature = create_webhook_signature(body, webhook_secret) + + with patch("reflector.settings.DAILY_WEBHOOK_SECRET", webhook_secret): + with patch("reflector.worker.process.process_recording_from_url.delay") as mock_task: + response = client.post( + "/v1/daily/webhook", + content=body, + headers={"X-Daily-Signature": signature} + ) + + assert response.status_code == 200 + mock_task.assert_called_once() + + +def test_invalid_signature(): + """Test webhook rejects invalid signature.""" + event_data = {"type": "participant.joined"} + body = json.dumps(event_data).encode() + + response = client.post( + "/v1/daily/webhook", + content=body, + headers={"X-Daily-Signature": "invalid"} + ) + + assert response.status_code == 401 +``` + +### Integration Tests + +**Test Checklist:** +- [ ] Platform factory creates correct client types +- [ ] Whereby client wrapper calls work +- [ ] Daily.co client API calls work (mocked) +- [ ] Webhook signature verification (both platforms) +- [ ] Recording download task executes +- [ ] Frontend components render correctly +- [ ] Platform routing works in RoomContainer + +### Manual Testing Procedure + +**Prerequisites:** +1. Daily.co account with API credentials +2. Webhook endpoint configured in Daily.co dashboard +3. Database migration applied + +**Test Scenario 1: Whereby Still Works** +```bash +# Set environment +export DEFAULT_VIDEO_PLATFORM=whereby +export DAILY_MIGRATION_ENABLED=false + +# Create room and meeting +# Verify Whereby embed loads +# Verify recording works +# Verify transcription pipeline runs +``` + +**Test Scenario 2: Daily.co New Installation** +```bash +# Set environment +export DEFAULT_VIDEO_PLATFORM=daily +export DAILY_MIGRATION_ENABLED=true +export DAILY_API_KEY=your-key +export DAILY_WEBHOOK_SECRET=your-secret + +# Create room and meeting +# Verify Daily.co iframe loads +# Verify participant events update count +# Start recording +# Verify webhook fires +# Verify recording downloads +# Verify transcription pipeline runs +``` + +**Test Scenario 3: Gradual Migration** +```bash +# Set environment +export DAILY_MIGRATION_ENABLED=true +export DAILY_MIGRATION_ROOM_IDS=["specific-room-id"] +export DEFAULT_VIDEO_PLATFORM=whereby + +# Create two rooms +# Verify one uses Daily, one uses Whereby +# Verify both work independently +``` + +--- + +## Rollout Plan + +### Phase 1: Development Testing (Week 1) +- [ ] Deploy to development environment +- [ ] Run full test suite +- [ ] Manual testing of both providers +- [ ] Performance benchmarking + +### Phase 2: Staging Validation (Week 2) +- [ ] Deploy to staging with `DAILY_MIGRATION_ENABLED=false` +- [ ] Verify no regressions in Whereby functionality +- [ ] Enable Daily.co for internal test rooms +- [ ] Validate recording pipeline end-to-end + +### Phase 3: Production Gradual Rollout (Weeks 3-6) +- [ ] Deploy to production with `DEFAULT_VIDEO_PLATFORM=whereby` +- [ ] Enable Daily.co for 1-2 beta customers +- [ ] Monitor error rates, recording success, transcription quality +- [ ] Gradually expand to more customers +- [ ] Collect feedback and iterate + +### Phase 4: Full Migration (Week 7+) +- [ ] Set `DEFAULT_VIDEO_PLATFORM=daily` for new installations +- [ ] Maintain Whereby support for existing customers +- [ ] Document platform selection in admin guide + +--- + +## Risk Analysis + +### Technical Risks + +| Risk | Impact | Likelihood | Mitigation | +|------|--------|------------|------------| +| Database migration fails on production | HIGH | LOW | Test migration on production copy first | +| Recording format incompatibility | HIGH | LOW | Both use MP4, validate with test recordings | +| Webhook signature fails | MEDIUM | LOW | Comprehensive tests, staging validation | +| Performance degradation from abstraction | MEDIUM | LOW | Benchmark before/after, <2% overhead target | +| Frontend component bugs | MEDIUM | MEDIUM | Extract Whereby logic first, test independently | +| Circular import issues | LOW | MEDIUM | Use TYPE_CHECKING pattern consistently | + +### Business Risks + +| Risk | Impact | Likelihood | Mitigation | +|------|--------|------------|------------| +| Customer complaints about new UI | MEDIUM | LOW | UI should be identical, consent flow same | +| Recording processing failures | HIGH | LOW | Same pipeline, tested with mock recordings | +| Whereby customers affected | HIGH | LOW | Feature flag off by default, no changes to Whereby flow | +| Cost overruns from dual providers | LOW | LOW | Provider selection controlled, not running both | + +--- + +## Success Metrics + +### Implementation Metrics +- [ ] Test coverage >90% for platform abstraction +- [ ] Zero failing tests in CI +- [ ] Database migration applies cleanly on staging +- [ ] All linting passes +- [ ] Documentation complete + +### Functional Metrics +- [ ] Whereby installations unaffected (0% regression) +- [ ] Daily.co meetings create successfully (>99%) +- [ ] Recording download success rate >98% +- [ ] Transcription quality equivalent between providers +- [ ] Webhook delivery rate >99.5% + +### Performance Metrics +- [ ] Meeting creation latency <500ms (both providers) +- [ ] Abstraction overhead <2% +- [ ] Frontend bundle size increase <50KB +- [ ] No memory leaks in long-running meetings + +--- + +## Documentation Requirements + +### Code Documentation +- [ ] Docstrings on all public methods +- [ ] Architecture decision records (ADR) for abstraction pattern +- [ ] Inline comments for complex logic + +### User Documentation +- [ ] Update README with provider configuration +- [ ] Admin guide for platform selection +- [ ] Troubleshooting guide for common issues + +### Developer Documentation +- [ ] Architecture diagram updated +- [ ] Contributing guide updated with platform addition process +- [ ] API documentation regenerated + +--- + +## Appendix A: File Checklist + +### Backend Files (New) +- [ ] `server/reflector/video_platforms/__init__.py` +- [ ] `server/reflector/video_platforms/base.py` +- [ ] `server/reflector/video_platforms/models.py` +- [ ] `server/reflector/video_platforms/registry.py` +- [ ] `server/reflector/video_platforms/factory.py` +- [ ] `server/reflector/video_platforms/whereby.py` +- [ ] `server/reflector/video_platforms/daily.py` +- [ ] `server/reflector/video_platforms/mock.py` +- [ ] `server/reflector/views/daily.py` +- [ ] `server/migrations/versions/YYYYMMDDHHMMSS_add_platform_support.py` + +### Backend Files (Modified) +- [ ] `server/reflector/settings.py` +- [ ] `server/reflector/views/rooms.py` +- [ ] `server/reflector/db/rooms.py` +- [ ] `server/reflector/db/meetings.py` +- [ ] `server/reflector/worker/process.py` +- [ ] `server/reflector/app.py` +- [ ] `server/env.example` + +### Frontend Files (New) +- [ ] `www/app/[roomName]/components/RoomContainer.tsx` +- [ ] `www/app/[roomName]/components/DailyRoom.tsx` +- [ ] `www/app/[roomName]/components/WherebyRoom.tsx` + +### Frontend Files (Modified) +- [ ] `www/app/[roomName]/page.tsx` +- [ ] `www/package.json` + +### Test Files (New) +- [ ] `server/tests/test_video_platforms.py` +- [ ] `server/tests/test_daily_webhook.py` +- [ ] `server/tests/utils/video_platform_test_utils.py` + +--- + +## Appendix B: Environment Variables Reference + +```bash +# Whereby Configuration (Existing) +WHEREBY_API_KEY= # API key from Whereby dashboard +WHEREBY_WEBHOOK_SECRET= # Webhook secret for signature verification +AWS_WHEREBY_S3_BUCKET= # S3 bucket for Whereby recordings +AWS_WHEREBY_ACCESS_KEY_ID= # AWS access key for S3 +AWS_WHEREBY_ACCESS_KEY_SECRET= # AWS secret key for S3 + +# Daily.co Configuration (New) +DAILY_API_KEY= # API key from Daily.co dashboard +DAILY_WEBHOOK_SECRET= # Webhook secret for signature verification +DAILY_SUBDOMAIN= # Your Daily.co subdomain (optional) +AWS_DAILY_S3_BUCKET= # S3 bucket for Daily.co recordings +AWS_DAILY_S3_REGION=us-west-2 # AWS region (default: us-west-2) +AWS_DAILY_ROLE_ARN= # IAM role ARN for S3 access + +# Platform Selection (New) +DAILY_MIGRATION_ENABLED=false # Master switch for Daily.co support +DAILY_MIGRATION_ROOM_IDS=[] # JSON array of specific room IDs for Daily +DEFAULT_VIDEO_PLATFORM=whereby # Default platform ("whereby" or "daily") +``` + +--- + +## Appendix C: Webhook Configuration + +### Daily.co Webhook Setup + +```bash +# Configure webhook endpoint via API +curl -X POST https://api.daily.co/v1/webhook-endpoints \ + -H "Authorization: Bearer ${DAILY_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://yourdomain.com/v1/daily/webhook", + "events": [ + "participant.joined", + "participant.left", + "recording.started", + "recording.ready-to-download", + "recording.error" + ] + }' +``` + +### Whereby Webhook Setup + +Configured via Whereby dashboard under Account Settings → Webhooks. + +--- + +## Summary + +This technical specification provides a complete, step-by-step guide for implementing multi-provider video platform support in Reflector. The implementation follows clean architecture principles, maintains backward compatibility, and enables zero-downtime migration between providers. + +**Key Implementation Principles:** +1. Abstraction before extension (Phase 2 before Phase 3) +2. Feature flags for gradual rollout +3. Comprehensive testing at each phase +4. Documentation alongside code +5. Monitor metrics throughout rollout + +**Estimated Timeline:** +- Phase 1: 2 hours (analysis) +- Phase 2: 4-5 hours (abstraction) +- Phase 3: 4-5 hours (Daily.co) +- Testing: 3-4 hours +- **Total: 13-16 hours** + +This document should be sufficient for a senior engineer to implement the feature independently with high confidence in the final result.