Integrating high-quality audio and video capabilities into standard web portals is highly demanding. When building telehealth platforms, developers must manage complex WebRTC signaling pipelines, handle varying local networks, and keep real-time communication synced with historical patient records in an EHR.

To bypass custom WebRTC server complexities, our telehealth patient platform leverages a highly elegant architectural combination: Azure Communication Services (ACS) for real-time video/chat session management, and Medplum as the secure, HIPAA-compliant FHIR engine. In this article, we will deep-dive into how the platform joins these systems inside React using custom brand composites and event-driven state machines.


Telehealth State Machine Architecture

Orchestrating video consultations requires moving patients and clinicians through a strict series of UI lifecycle steps, ensuring media streams are turned on only at the appropriate moments:


Step 1: Bootstrapping the ACS Call Composite

Azure Communication Services provides high-level UI composites via @azure/communication-react. We hook into the platform's state machine, initialize an ACS Token Credential using an authenticated token, and mount the Call Composite wrapper using our brand colors:

import { AzureCommunicationTokenCredential } from '@azure/communication-common';
import { 
    CallWithChatComposite, 
    FluentThemeProvider, 
    createAzureCommunicationCallWithChatAdapter 
} from '@azure/communication-react';

const joinCall = useCallback(async (): Promise<void> => {
    if (!accessToken || !acsGroupId || !acsChatThreadId || !userId || !endpoint) return;

    setLoading(true);
    try {
        const tokenCredential = new AzureCommunicationTokenCredential(accessToken);
        const locator = {
            callLocator: { groupId: acsGroupId },
            chatThreadId: acsChatThreadId,
        };

        // Create unified ACS adapter
        const callAdapter = await createAzureCommunicationCallWithChatAdapter({
            userId: { communicationUserId: userId },
            displayName: patientName || 'Patient',
            credential: tokenCredential,
            endpoint,
            locator,
        });

        // Initially join with camera and microphone muted
        callAdapter.joinCall({ cameraOn: false, microphoneOn: false });
        setAdapter(callAdapter);
        setStep('waiting');
    } catch (err) {
        console.error('Failed to bootstrap ACS Adapter:', err);
        setError('Failed to initialize session.');
    } finally {
        setLoading(false);
    }
}, [accessToken, acsGroupId, acsChatThreadId, userId, endpoint]);

Step 2: Event-Driven Participant Detection

Telehealth platforms must ensure patients do not sit in empty calls wasting local battery and bandwidth. We solve this by joining in a silent state, registering event listeners on the ACS adapter, and programmatically starting the local camera and microphone ONLY when the provider actually joins the room:

useEffect(() => {
    if (!adapter || step !== 'waiting') return;

    const onParticipantsJoined = (): void => {
        // Clinician joined - launch camera, unmute local mic, and start call
        adapter.startCamera().catch(console.error);
        adapter.unmute().catch(console.error);
        
        setStep('active-call');
        
        // Start call duration timer
        setElapsedSeconds(0);
        timerRef.current = setInterval(() => {
            setElapsedSeconds(prev => prev + 1);
        }, 1000);
    };

    adapter.on('callParticipantsJoined', onParticipantsJoined);
    return () => {
        adapter.off('callParticipantsJoined', onParticipantsJoined);
    };
}, [adapter, step]);

Step 3: Post-Call Medplum Document Retrieval

Once the call ends, we transition immediately to the post-call stage. We query the Medplum-hosted appointment documents via a secure AWS Lambda bot proxy route to fetch clinical notes written by the provider in real-time:

useEffect(() => {
    if (step !== 'post-call' || !appointmentId) return;

    const fetchClinicalNotes = async (): Promise<void> => {
        setLoadingNotes(true);
        try {
            const response = await fetch(botLambdaUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    action: 'get-appointment-documents',
                    appointmentId,
                }),
            });
            const data = await response.json();
            if (data.success) {
                const notes = data.documents
                    .filter((d: any) => d.type === 'clinical-note')
                    .map((d: any) => ({
                        id: d.id,
                        title: d.title,
                        textContent: d.textContent,
                        date: d.date,
                    }));
                setClinicalNotes(notes);
            }
        } catch (err) {
            console.error('Failed to sync appointment notes:', err);
        } finally {
            setLoadingNotes(false);
        }
    };

    fetchClinicalNotes().catch(console.error);
}, [step, appointmentId]);

Telehealth Architecture Best Practices

  1. Silent Waiting Lobby: Mute all local audio/video hardware on initial connection to minimize system resources and guarantee patient privacy prior to clinician arrival.
  2. Fluent UI Customization: Use FluentThemeProvider wrappers with brand palette schemes to blend calling composites into standard web themes.
  3. EHR Synced Summaries: Fetch clinical summaries dynamically using secure appointment tokens on call termination to close the care cycle instantly.

Combining Azure's scalable Communication Service with Medplum's secure, FHIR-compliant EHR framework creates an exceptionally robust, enterprise-grade telehealth infrastructure that delivers seamless patient-clinician consultations at scale.