-- Migration 004: Voice/Video Calls System -- WebRTC calling infrastructure for 1-on-1 and group calls -- ============================================================================ -- CALLS TABLE -- ============================================================================ CREATE TABLE IF NOT EXISTS calls ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, type VARCHAR(20) NOT NULL CHECK (type IN ('voice', 'video')), initiator_id UUID NOT NULL REFERENCES auth.users(id), status VARCHAR(30) NOT NULL DEFAULT 'ringing' CHECK (status IN ('ringing', 'active', 'ended', 'failed', 'missed')), sfu_room_id VARCHAR(100), started_at TIMESTAMP WITH TIME ZONE, ended_at TIMESTAMP WITH TIME ZONE, duration_seconds INTEGER, end_reason VARCHAR(50), metadata JSONB, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_calls_conversation ON calls(conversation_id); CREATE INDEX IF NOT EXISTS idx_calls_initiator ON calls(initiator_id); CREATE INDEX IF NOT EXISTS idx_calls_status ON calls(status); CREATE INDEX IF NOT EXISTS idx_calls_created ON calls(created_at DESC); -- ============================================================================ -- CALL PARTICIPANTS -- ============================================================================ CREATE TABLE IF NOT EXISTS call_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id), joined_at TIMESTAMP WITH TIME ZONE, left_at TIMESTAMP WITH TIME ZONE, media_state JSONB DEFAULT '{"audio": true, "video": false, "screen": false}'::jsonb, connection_quality VARCHAR(20) DEFAULT 'unknown' CHECK (connection_quality IN ('excellent', 'good', 'fair', 'poor', 'unknown')), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(call_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_call_participants_call ON call_participants(call_id); CREATE INDEX IF NOT EXISTS idx_call_participants_user ON call_participants(user_id); -- ============================================================================ -- TURN CREDENTIALS (for NAT traversal) -- ============================================================================ CREATE TABLE IF NOT EXISTS turn_credentials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, username VARCHAR(200) NOT NULL, credential VARCHAR(500) NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(user_id) ); CREATE INDEX IF NOT EXISTS idx_turn_credentials_user ON turn_credentials(user_id); CREATE INDEX IF NOT EXISTS idx_turn_credentials_expires ON turn_credentials(expires_at); -- ============================================================================ -- ENABLE ROW LEVEL SECURITY -- ============================================================================ ALTER TABLE calls ENABLE ROW LEVEL SECURITY; ALTER TABLE call_participants ENABLE ROW LEVEL SECURITY; ALTER TABLE turn_credentials ENABLE ROW LEVEL SECURITY; -- ============================================================================ -- RLS POLICIES -- ============================================================================ -- Calls: Users can see calls they're participants in CREATE POLICY "calls_select" ON calls FOR SELECT USING ( EXISTS ( SELECT 1 FROM call_participants WHERE call_participants.call_id = calls.id AND call_participants.user_id = auth.uid() ) OR initiator_id = auth.uid() ); CREATE POLICY "calls_insert" ON calls FOR INSERT WITH CHECK (initiator_id = auth.uid()); CREATE POLICY "calls_update" ON calls FOR UPDATE USING ( initiator_id = auth.uid() OR EXISTS ( SELECT 1 FROM call_participants WHERE call_participants.call_id = calls.id AND call_participants.user_id = auth.uid() ) ); -- Call Participants: Users can see participants in their calls CREATE POLICY "call_participants_select" ON call_participants FOR SELECT USING ( user_id = auth.uid() OR EXISTS ( SELECT 1 FROM call_participants cp2 WHERE cp2.call_id = call_participants.call_id AND cp2.user_id = auth.uid() ) ); CREATE POLICY "call_participants_insert" ON call_participants FOR INSERT WITH CHECK ( user_id = auth.uid() OR EXISTS ( SELECT 1 FROM calls WHERE id = call_id AND initiator_id = auth.uid() ) ); CREATE POLICY "call_participants_update" ON call_participants FOR UPDATE USING (user_id = auth.uid()); -- TURN Credentials: Users can only see their own CREATE POLICY "turn_credentials_select" ON turn_credentials FOR SELECT USING (user_id = auth.uid()); CREATE POLICY "turn_credentials_insert" ON turn_credentials FOR INSERT WITH CHECK (user_id = auth.uid()); CREATE POLICY "turn_credentials_update" ON turn_credentials FOR UPDATE USING (user_id = auth.uid()); -- ============================================================================ -- TRIGGER TO TRACK CALL DURATION -- ============================================================================ CREATE OR REPLACE FUNCTION update_call_duration() RETURNS TRIGGER AS $$ BEGIN IF NEW.status = 'ended' AND OLD.status != 'ended' THEN NEW.ended_at = NOW(); IF NEW.started_at IS NOT NULL THEN NEW.duration_seconds = EXTRACT(EPOCH FROM (NEW.ended_at - NEW.started_at))::INTEGER; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trigger_call_duration ON calls; CREATE TRIGGER trigger_call_duration BEFORE UPDATE ON calls FOR EACH ROW EXECUTE FUNCTION update_call_duration();