Cómo crear consultas privadas por vídeo en Next.js

Una consulta por vídeo parece una sola llamada, y en una app de Next.js son varias funciones. Alguien reserva media hora con una médica, un abogado, una tutora o un orientador, y esa hora desaparece de todas las páginas a la vez, para que nadie más la reserve. A su hora, quien reservó espera, y su especialista, en cualquier página en la que esté, ve que espera y le deja entrar. La visita es una videollamada de las dos personas a la que nadie más puede unirse, y que el servicio que la transporta no puede ver ni oír. Al lado, los mensajes y archivos de la cita, guardados en tu propio servidor, y después, las notas de su especialista.
Este artículo recorre cada parte que necesita una app real, con un ejemplo completo que funciona: Consultations, el sitio de consultas privadas de nuestra guía de Next.js, donde se reserva a especialistas, se espera en una sala de espera y se habla con ellos por vídeo. Puedes probarlo en vivo: sé Ava, cuya visita con Lena está abierta ahora, y pulsa I'm ready. Luego abre el sitio en una ventana privada como Lena, que ve a Ava en la sala de espera, y deja entrar a Ava.
Lo que necesita una consulta por vídeo
- Una reserva que se mantiene. Las horas disponibles en cada página, y una hora que desaparece de todas en cuanto alguien la reserva.
- Una sala de espera. Quien reservó dice que está listo, y su especialista lo ve esperar desde cualquier página, no solo la de la visita.
- Dejar entrar. Solo las dos personas de la cita, solo en torno a su hora, y quien reservó solo cuando su especialista le deja entrar.
- Privacidad. La cámara y el micrófono cifrados de extremo a extremo, para que los servidores que transportan la llamada reenvíen lo que no pueden ver ni oír.
- Mensajes y archivos al lado, guardados donde guardas tus datos, y notas para después.
- Un final. La visita se cierra al terminar su media hora, y nadie vuelve a entrar.
Puedes construir todo esto por tu cuenta, con WebRTC, servidores de medios y un servidor de websockets propio. O dejas que una nube en tiempo real se encargue de los medios, de la sala de espera y de la pantalla de llamada, mientras tu servidor conserva lo que de verdad es tuyo: las reservas, las reglas y los archivos. Eso es lo que hace Consultations, a través de WordPlus Cloud.
El ejemplo: un sitio de consultas
Especialistas ofrecen medias horas: una médica de familia, un abogado, una tutora, un orientador. Las personas que consultan reservan una hora, o hablan ahora mismo con un especialista disponible. Cada cita tiene su propia página: su visita, sus mensajes y, cuando la visita termina, las notas de su especialista. Es una app de Next.js con App Router, Better Auth y Postgres, y su parte en tiempo real son unos pocos archivos. Cinco pasos construyen sus visitas.
1. Horas que desaparecen al reservarse
Se reserva con una server action. El servidor la guarda, y la propia base de datos rechaza una media hora ya ocupada. Después lo comunica a través de WordPlus Cloud, dos veces:
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() llega a cada página que el especialista tiene abierta, en cualquier dispositivo, con un aviso de la nueva visita. experts es una sala a la que se une cada página con sesión iniciada, y en la que solo habla el servidor. Cada página oye changed y vuelve a pedirse a su servidor, sin recargar:
useRoom('experts', { changed: () => router.refresh() });Quien estaba mirando las horas de ese especialista ve desaparecer la media hora. La misma sala avisa a las páginas cuando una visita empieza o termina, así que también se mantiene al día quién está disponible ahora mismo. WordPlus Cloud sabe qué visitas están en curso (realtime.listCalls()), y el sitio no guarda ninguna tabla para ello.
2. La sala de espera
Cada cita tiene una sala de espera, una sala de presencia con su nombre:
realtime.room('waiting:{id}', { presence: true }, async (user, { id }) => mayChat(user, id));Solo entran las dos personas de la cita. Cuando quien reservó pulsa I'm ready, su página se une a la sala y se queda en ella mientras la página siga abierta. La página del especialista lee quién está dentro:
function useWaiting(id: string, client: string): boolean {
return usePresence(`waiting:${id}`).some((member) => String(member.id) === client);
}La presencia sigue a la página: cerrar la pestaña basta para salir de la sala de espera. Y el especialista no tiene que estar en la página de la visita. El servidor le avisa, con un evento waiting a través de realtime.toUser(), y en cualquier página aparece un aviso, «Ava is in the waiting room», que abre la visita.
3. Dejar entrar
La visita es una llamada en grupo con el nombre de la cita, visit:81, declarada una vez, por ese patrón, con su regla:
realtime.groupCalls('visit:{id}', async (user, { id }) => mayJoinVisit(user, id), { encrypt: true });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;
}Las dos personas de la cita y nadie más, mientras la visita está abierta: desde diez minutos antes de su hora hasta el final de su media hora, salvo que termine antes. Su especialista entra cuando quiera mientras está abierta, y quien reservó, una vez que se le deja entrar. Dejar entrar es una server action, que marca la cita y avisa a quien reservó:
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;
}A partir de entonces la regla dice que sí, y la página que esperaba entra en la visita por sí sola. Cuando una página pide unirse, pregunta primero al servidor del propio sitio, en /api/realtime/calls: el mismo route handler por el que pasan las llamadas del sitio de citas (cómo implementar videollamadas en Next.js). La regla se ejecuta con la sesión del propio sitio, y la página recibe un pase para la visita, firmado con el secreto de la app, o el motivo por el que no.
4. La visita, cifrada de extremo a extremo
{ encrypt: true } en la regla hace que la visita vaya cifrada de extremo a extremo. Cada navegador cifra su cámara y su micrófono con una clave que crea el servidor del sitio, y que solo reciben aquellos a quienes la regla deja entrar. Las claves se quedan en el sitio y en los dos navegadores, y los servidores de medios de WordPlus Cloud reenvían lo que no pueden ver ni oír (cómo funcionan los servidores de medios).
La visita se muestra en la propia página de la cita, donde la página la coloca:
<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>Antes de la visita, CallSpot muestra lo que lleva dentro: la sala de espera tal como la ve cada una de las dos personas, con Let Ava in para el especialista. join lleva la página a la visita, en cuanto el especialista deja entrar o en cuanto se deja entrar a quien espera. joinWith="microphone camera" enciende ambos al entrar, porque en una visita hay que verse y oírse desde el primer momento. En otra página de la app, la visita sigue en una ventana en la esquina, y vuelve aquí cuando se abre de nuevo la página de la cita.
Una visita también termina. El especialista la termina con un botón, y el sitio la termina al final de su media hora, con un temporizador que pone cuando empieza la visita:
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() saca a ambos de la llamada, la regla, al ver la cita terminada, ya no deja entrar a nadie, y las dos páginas dicen que la visita ha terminado.
5. Mensajes y archivos, y después las notas
Cada cita tiene una conversación, la sala chat:{id}, solo para sus dos personas, antes de la visita, durante y después. El Chat ya hecho la muestra junto a la visita, con quién escribe, confirmaciones de lectura y archivos:
<Chat
rt={useRealtime()}
room={room}
messages={`/api/messages?room=${room}`}
upload="/api/files"
accept="image/*,.pdf,.doc,.docx,.txt"
maxFileSize={10 * 1024 * 1024}
/>Una foto, un escaneo o un contrato sube a la ruta propia del sitio en cuanto se elige, y la ruta consulta la regla de la sala antes de guardarlo:
if (!me || !appointment || !(file instanceof File) || !(await realtime.mayJoin(room, me))) {
return Response.json({ error: 'This file is not for this conversation.' }, { status: 403 });
}El sitio guarda el archivo en su propia base de datos y solo lo abre a las dos personas. WordPlus Cloud transporta únicamente el mensaje que dice dónde está, sellado con una clave que tiene el sitio, y nunca el archivo. El sitio guarda también cada mensaje, desde el webhook message, así que la conversación sigue ahí para ambos después de la visita. Cuando la visita termina, el especialista escribe notas, que el sitio guarda, y la página de quien reservó las muestra al instante, de nuevo con toUser().
Lo que viene incluido
- Hablar ahora. Quien consulta pide a un especialista disponible una sesión en ese momento. La página del especialista muestra la petición, con un minuto para aceptarla, y ambos entran directamente en una visita nueva.
- Quién está disponible, al día en cada página. Un especialista en una visita aparece como ocupado, desde
realtime.listCalls(), sin ninguna tabla tuya. - En línea o no. La sala de espera dice si su especialista ya está en línea, con
useOnline(). - Nombres y caras de tu app. Cada recuadro y cada mensaje muestran un nombre y una foto tal como los da el
profiledel sitio, sin ninguna lista de usuarios guardada en WordPlus Cloud.
Pruébalo y luego construye el tuyo
En la demo en vivo, especialistas y clientes son solo tuyos durante un día, así que nadie más ve lo que escribas o con quién te encuentres, y una visita termina sola a los 10 minutos. Bajo cada página, How this page works muestra el código que la hace funcionar, paso a paso, con los valores de la página que tienes abierta.
La guía de consultas privadas construye el sitio completo, con cada archivo de su parte en tiempo real, las llamadas en Next.js reúnen todas las formas de poner una llamada en tu propia app, y el cifrado explica qué va sellado y quién tiene cada clave. Para la reunión de todo un equipo en lugar de dos personas, lee cómo implementar videoconferencias en Next.js.
WordPlus Cloud for Apps tiene un precio según las conexiones simultáneas, a partir de 1.000, con facturación mensual o anual. Una visita es una llamada en grupo de dos personas. Con 1.000 conexiones, las llamadas en grupo no se miden hasta 32 personas con vídeo a la vez, es decir, 16 visitas, contando como media a una persona solo con audio, y cada 1.000 conexiones más añaden 12. Por encima, cada persona adicional con vídeo gasta un crédito por minuto, y las llamadas entre dos personas no gastan ninguno. Descubre las videollamadas para apps, o consulta los precios para apps. ¿Preguntas sobre tu integración? Escríbenos.