Blog

Zo bouw je privé videoconsulten in Next.js

· Andrij Tkachenko · 9 min leestijd

Een browservenster met een videoconsult van twee mensen, een hangslot in de bovenbalk en de gespreksknoppen eronder, naast een kolom met berichten en een bestand met een paperclip

Een videoconsult lijkt één gesprek, en in een Next.js-app zijn het meerdere functies. Iemand boekt een half uur bij een arts, een advocaat, een bijlesdocent of een counselor, en dat tijdstip verdwijnt meteen van elke pagina, zodat niemand anders het boekt. Op het afgesproken moment wacht de cliënt, en de specialist ziet dat, op welke pagina die ook is, en laat de cliënt binnen. Het bezoek is een videogesprek van die twee waar niemand anders bij kan, en dat de dienst die het doorgeeft niet kan zien of horen. Ernaast de berichten en bestanden van de afspraak, bewaard door je eigen server, en daarna de notities van de specialist.

Deze post loopt elk onderdeel door dat een echte app nodig heeft, met een compleet werkend voorbeeld: Consultations, de site voor privéconsulten uit onze Next.js-gids, waar cliënten specialisten boeken, in een wachtkamer wachten en ze op video spreken. Je kunt het live proberen: wees Ava, met een bezoek bij Lena dat nu open is, en klik op I'm ready. Open de site daarna in een privévenster als Lena, die Ava in de wachtkamer ziet, en laat Ava binnen.

Wat een videoconsult nodig heeft

  • Een boeking die standhoudt. De beschikbare tijden op elke pagina, en een tijd die van allemaal verdwijnt zodra iemand die boekt.
  • Een wachtkamer. De cliënt laat weten klaar te zijn, en de specialist ziet het wachten op elke pagina, niet alleen op die van het bezoek.
  • Binnenlaten. Alleen de twee van de afspraak, alleen rond het tijdstip ervan, en de cliënt pas als de specialist de cliënt binnenlaat.
  • Privacy. Camera en microfoon end-to-end versleuteld, zodat de servers die het gesprek doorgeven doorsturen wat ze niet kunnen zien of horen.
  • Berichten en bestanden ernaast, bewaard waar je je gegevens bewaart, en notities voor daarna.
  • Een einde. Het bezoek sluit aan het eind van zijn half uur, en niemand komt er nog in.

Je kunt dit allemaal zelf bouwen, met WebRTC, mediaservers en een eigen websocketserver. Of je laat een realtime cloud de media, de wachtkamer en het gespreksscherm draaien, terwijl je server houdt wat echt van jou is: de boekingen, de regels en de bestanden. Dat is wat Consultations doet, via WordPlus Cloud.

Het voorbeeld: een site voor consulten

Specialisten bieden halve uren aan: een huisarts, een advocaat, een bijlesdocent, een counselor. Cliënten boeken een tijd, of spreken meteen een specialist die nu beschikbaar is. Elke afspraak heeft een eigen pagina: het bezoek, de berichten en, als het bezoek voorbij is, de notities van de specialist. Het is een Next.js-app met de App Router, Better Auth en Postgres, en het realtime deel bestaat uit een paar bestanden. Vijf stappen maken de bezoeken.

1. Tijden die verdwijnen zodra ze geboekt zijn

Boeken gaat met een server action. De server bewaart de boeking, en een half uur dat al bezet is weigert de database zelf. Daarna meldt de server het via WordPlus Cloud, twee keer:

app/(app)/actions.ts
export async function bookVisit(expert: string, startsAt: number, topic: string): Promise<{ error: string }> {
    const me = await currentUserId();
    if (!me) return { error: 'Sign in first.' };
    const booked = await book(me, expert, startsAt, topic);
    if (typeof booked === 'string') return { error: booked };
    await realtime.toUser(expert).emit('booked', { appointment: booked.id, name: booked.clientName, startsAt: booked.startsAt });
    // Every page showing this expert's times takes this one away.
    await realtime.to('experts').emit('changed');
    redirect(`/appointments/${booked.id}`);
}

toUser() bereikt elke pagina die de specialist open heeft, op elk apparaat, met een melding van het nieuwe bezoek. experts is een room waar elke ingelogde pagina aan deelneemt, en waarin alleen de server spreekt. Elke pagina hoort changed en vraagt zichzelf opnieuw op bij de server, zonder herladen:

app/(app)/live.tsx
useRoom('experts', { changed: () => router.refresh() });

Wie net naar de tijden van die specialist keek, ziet het half uur verdwijnen. Dezelfde room laat de pagina’s weten wanneer een bezoek begint of eindigt, zodat ook klopt wie op dit moment beschikbaar is. WordPlus Cloud weet welke bezoeken bezig zijn (realtime.listCalls()), en de site houdt daar geen tabel voor bij.

2. De wachtkamer

Elke afspraak heeft een wachtkamer, een presence-room met de naam van de afspraak:

lib/realtime.ts
realtime.room('waiting:{id}', { presence: true }, async (user, { id }) => mayChat(user, id));

Alleen de twee van de afspraak komen erin. Zodra de cliënt op I'm ready klikt, neemt de pagina deel aan de room en blijft erin zolang de pagina open is. De pagina van de specialist leest wie erin zit:

app/(app)/appointments/[id]/visit.tsx
function useWaiting(id: string, client: string): boolean {
    return usePresence(`waiting:${id}`).some((member) => String(member.id) === client);
}

Presence volgt de pagina: het tabblad sluiten is genoeg om de wachtkamer te verlaten. En de specialist hoeft niet op de pagina van het bezoek te zijn. De server meldt het, met een waiting-event via realtime.toUser(), en op elke pagina verschijnt een melding, „Ava is in the waiting room”, die het bezoek opent.

3. Binnenlaten

Het bezoek is een groepsgesprek met de naam van de afspraak, visit:81, één keer gedeclareerd, met dat patroon, samen met de regel ervan:

lib/realtime.ts
realtime.groupCalls('visit:{id}', async (user, { id }) => mayJoinVisit(user, id), { encrypt: true });
lib/clinic.ts
export async function mayJoinVisit(user: string | number | null, id: string): Promise<boolean> {
    const appointment = await appointmentOf(id);
    if (!appointment || !isParty(appointment, user) || !visitOpen(appointment)) return false;
    return String(user) === appointment.expert || appointment.admittedAt !== null;
}

De twee van de afspraak en niemand anders, zolang het bezoek open is: van tien minuten voor het tijdstip tot het eind van het half uur, tenzij het eerder eindigde. De specialist komt binnen wanneer het open is, de cliënt pas na het binnenlaten. Binnenlaten is een server action, die de afspraak markeert en de cliënt inlicht:

app/(app)/actions.ts
export async function letIn(id: string): Promise<boolean> {
    const me = await currentUserId();
    const appointment = me ? await admit(id, me) : null;
    if (!appointment) return false;
    await realtime.toUser(appointment.client).emit('admitted', { appointment: id, name: appointment.expertName });
    return true;
}

Vanaf dan zegt de regel ja, en de wachtende pagina komt vanzelf het bezoek binnen. Als een pagina wil deelnemen, vraagt ze het eerst aan de eigen server van de site, op /api/realtime/calls: dezelfde route handler waar de gesprekken van de datingsite doorheen gaan (zo implementeer je videogesprekken in Next.js). De regel draait met de eigen sessie van de site, en de pagina krijgt een pas voor het bezoek, ondertekend met het geheim van de app, of de reden waarom niet.

4. Het bezoek, end-to-end versleuteld

{ encrypt: true } in de regel maakt het bezoek end-to-end versleuteld. Elke browser versleutelt camera en microfoon met een sleutel die de server van de site maakt, en die alleen krijgt wie de regel binnenlaat. De sleutels blijven bij de site en in de twee browsers, en de mediaservers van WordPlus Cloud sturen door wat ze niet kunnen zien of horen (hoe mediaservers werken).

Het bezoek staat in de pagina van de afspraak zelf, waar de pagina het neerzet:

app/(app)/appointments/[id]/visit.tsx
<CallSpot rt={rt} call={`visit:${id}`} join={join} joinWith="microphone camera" className="h-[32rem] rounded-2xl">
    {/* The waiting room as each of the two sees it, until the page joins */}
</CallSpot>

Voor het bezoek toont CallSpot wat erin staat: de wachtkamer zoals elk van de twee die ziet, met Let Ava in voor de specialist. join brengt de pagina in het bezoek, zodra de specialist binnenlaat, of zodra wie wacht binnengelaten wordt. joinWith="microphone camera" zet bij binnenkomst allebei aan, want bij een bezoek wil je elkaar meteen zien en horen. Op een andere pagina van de app loopt het bezoek door in een venster in de hoek, en komt het hier terug zodra de pagina van de afspraak weer open is.

Een bezoek eindigt ook. De specialist beëindigt het met een knop, en de site beëindigt het aan het eind van het half uur, met een timer die de site zet als het bezoek begint:

lib/realtime.ts
export async function endVisit(appointment: Appointment) {
    await realtime.call(visitOf(appointment.id)).end().catch(() => {}); // not on, if nobody came in
    await markEnded(appointment.id);
    await realtime.toUser([appointment.expert, appointment.client]).emit('ended', { appointment: appointment.id });
    await realtime.to('experts').emit('changed');
}

end() haalt allebei uit het gesprek, de regel ziet dat de afspraak voorbij is en laat niemand meer binnen, en beide pagina’s melden dat het bezoek voorbij is.

5. Berichten en bestanden, en daarna de notities

Elke afspraak heeft een gesprek, de room chat:{id}, alleen voor de twee, voor het bezoek, tijdens en erna. De kant-en-klare Chat toont het naast het bezoek, met wie er typt, leesbevestigingen en bestanden:

app/(app)/appointments/[id]/messages.tsx
<Chat
    rt={useRealtime()}
    room={room}
    messages={`/api/messages?room=${room}`}
    upload="/api/files"
    accept="image/*,.pdf,.doc,.docx,.txt"
    maxFileSize={10 * 1024 * 1024}
/>

Een foto, een scan of een contract gaat naar de eigen route van de site zodra je het kiest, en de route vraagt de regel van de room voordat die het bewaart:

app/api/files/route.ts
if (!me || !appointment || !(file instanceof File) || !(await realtime.mayJoin(room, me))) {
    return Response.json({ error: 'This file is not for this conversation.' }, { status: 403 });
}

De site bewaart het bestand in de eigen database en opent het alleen voor de twee. WordPlus Cloud vervoert alleen het bericht dat zegt waar het staat, verzegeld met een sleutel die de site bezit, en nooit het bestand zelf. De site bewaart ook elk bericht, vanuit de webhook message, zodat het gesprek na het bezoek voor allebei blijft staan. Is het bezoek voorbij, dan schrijft de specialist notities, die de site bewaart, en de pagina van de cliënt toont ze meteen, weer via toUser().

Wat erbij zit

  • Nu spreken. Een cliënt vraagt een specialist die nu beschikbaar is om een gesprek op dit moment. De pagina van de specialist toont het verzoek, met een minuut om het aan te nemen, en allebei gaan ze meteen een nieuw bezoek in.
  • Wie beschikbaar is, actueel op elke pagina. Een specialist in een bezoek staat als bezet, uit realtime.listCalls(), zonder tabel van jou.
  • Online of niet. De wachtkamer laat zien of de specialist al online is, met useOnline().
  • Namen en gezichten uit je app. Elke tegel en elk bericht toont een naam en foto zoals het profile van de site ze geeft, zonder lijst met gebruikers in WordPlus Cloud.

Probeer het, en bouw dan je eigen

In de live demo zijn de specialisten en cliënten een dag lang alleen van jou, dus wat je daar schrijft of wie je daar ontmoet, ziet niemand anders, en een bezoek eindigt na 10 minuten vanzelf. Onder elke pagina laat How this page works stap voor stap de code zien die haar maakt, met de waarden van de pagina die je open hebt.

De gids voor privéconsulten bouwt de hele site, met elk bestand van het realtime deel, gesprekken in Next.js laat elke manier zien om een gesprek in je eigen app te zetten, en versleuteling legt uit wat verzegeld is en wie welke sleutel heeft. Voor een vergadering van een heel team in plaats van twee mensen, lees zo implementeer je videovergaderingen in Next.js.

WordPlus Cloud for Apps is geprijsd op gelijktijdige verbindingen, vanaf 1.000, maandelijks of jaarlijks gefactureerd. Een bezoek is een groepsgesprek van twee. Met 1.000 verbindingen worden groepsgesprekken niet gemeten tot 32 mensen tegelijk op video, dus 16 bezoeken, waarbij iemand op audio voor de helft telt, en elke 1.000 verbindingen extra voegen er 12 toe. Daarboven kost elke extra persoon op video een credit per minuut, en gesprekken tussen twee mensen kosten geen credits. Bekijk videogesprekken voor apps, of de prijzen voor apps. Vragen over je integratie? Schrijf ons.