Blog

Cómo crear un chat entre compradores y vendedores para un marketplace en Next.js

· Andrij Tkachenko · 9 min de lectura

Una ventana de navegador con la ficha de un artículo a la izquierda, un tocadiscos con una etiqueta de precio y dos botones para una oferta, y una conversación a la derecha, burbujas de mensajes entre dos personas y un recuadro de vídeo en el chat

Un chat entre compradores y vendedores parece una sola función, y en un marketplace hecho con Next.js son varias. Quien compra pregunta por un artículo y recibe la respuesta mientras todavía lo está mirando. La conversación es solo de esas dos personas, con fotos que nadie más puede abrir. A veces una foto no basta, y una videollamada corta muestra el artículo antes de que nadie se desplace. Una oferta tiene que llegar a quien vende en cualquier página de la app, y la respuesta tiene que volver de la misma forma. Y todas las personas que miran un artículo deberían ver su precio, y si sigue a la venta, en cuanto cambia.

Este artículo recorre cada parte que necesita una app real, con un ejemplo completo que funciona: el marketplace de nuestra guía de Next.js, donde los vecinos compran y venden sus cosas. Puedes probarlo en vivo: entra como Omar, que vende el tocadiscos, y después abre el sitio en una ventana privada como Ava, con una oferta de 130 $ esperando respuesta. Cambia la oferta como Ava, y Omar se entera al instante, esté en la página que esté.

Lo que necesita el chat de un marketplace

  • Una conversación por artículo y comprador. Cada persona que pregunta por un artículo tiene su propia conversación con quien lo vende, y nadie más la lee, ni otros compradores ni otros vendedores.
  • Mensajes que llegan mientras miras. Un mensaje pasa de página en página al enviarse, con el aviso de quien escribe y la confirmación de lectura, y se guarda en tu propia base de datos. Las conexiones que eso requiere siguen abiertas mientras la página lo está, y una función serverless no puede mantenerlas (por qué, y qué hacer al respecto).
  • Fotos guardadas por ti. Quien compra quiere ver algo de cerca, y quien vende envía una foto. Las fotos se quedan en tu servidor, y solo las dos personas de la conversación pueden abrirlas.
  • Una llamada para ver el artículo. En vídeo, desde la conversación, antes de que nadie vaya a recogerlo.
  • Ofertas que ninguna página puede falsificar. Una oferta, y el sí o el no de quien vende, salen de tu servidor, nunca de una página, y las dos partes las ven al momento.
  • El marketplace tal como está ahora. Cuántas personas más miran un artículo, su nuevo precio, y si se reservó o se vendió, en cada página que lo muestra.

Puedes construir todo esto por tu cuenta, con websockets y WebRTC propios. O puedes dejar que una nube en tiempo real lleve los mensajes, las llamadas y los recuentos, mientras tu servidor se queda con lo que de verdad es tuyo: quién puede hablar con quién, las conversaciones y las ofertas. Eso es lo que hace el marketplace, a través de WordPlus Cloud.

El ejemplo: un marketplace

Los vecinos publican sus cosas con fotos y un precio. Quien quiere comprar abre un artículo, pulsa Message Omar, y se abre la conversación sobre él, con el artículo y la oferta encima del chat. Es una app de Next.js con el App Router, Better Auth y Postgres, y su parte en tiempo real son unos pocos archivos. Cinco pasos forman sus conversaciones.

1. Una conversación por artículo y comprador

Cada conversación es una sala con el nombre de su número en la base de datos, deal:7, y reúne un artículo, a quien lo vende y a una persona que compra. El sitio declara estas salas una vez, con ese patrón, con lo que una página puede enviar en ellas y con la regla:

lib/realtime.ts
realtime.room(
    'deal:{id}',
    { history: 50, emit: ['message', 'typing', 'reaction', 'read'], webhook: ['message', 'reaction'] },
    async (user, { id }) => mayTalk(user, id),
);
lib/market.ts
export async function mayTalk(user: string | number | null, deal: string): Promise<boolean> {
    return user !== null && (await partiesOf(deal)).includes(String(user));
}

mayTalk pregunta a la base de datos quiénes son las dos personas de la conversación, y no deja entrar a nadie más. Una página pide entrar en la ruta propia del sitio, /api/realtime/auth, que ejecuta la regla con la sesión del propio sitio y responde con un pase firmado con el secreto de la app, o con el motivo de la negativa. Después, los mensajes, los avisos de escritura, las reacciones y las confirmaciones de lectura van de página en página a través de WordPlus Cloud, y webhook envía también cada mensaje y cada reacción al sitio, que los guarda en Postgres:

lib/realtime.ts
realtime.on('message', async ({ room, from, data, event_id, at }) => {
    const deal = idIn(room, 'deal');
    if (!from || !deal) return;
    await keepAndTell({ id: event_id, deal, from: String(from), text: String(data.text ?? ''), files: data.files as unknown[], at });
});

keepAndTell guarda el mensaje una sola vez, por el id de su evento, y avisa a la otra persona de la conversación con realtime.toUser(), que llega a cada página que esa persona tiene abierta: un aviso que abre la conversación.

2. El chat en la página, con las fotos en tu servidor

La conversación es un chat ya hecho, el de Better Messages, en tu propio árbol:

app/(app)/messages/[id]/chat.tsx
<Chat
    rt={useRealtime()}
    room={room}
    to={to}
    name={name}
    avatar={avatar}
    call
    messages={`/api/messages?room=${room}`}
    upload="/api/files"
    accept="image/jpeg,image/png,image/webp,image/gif"
    maxFileSize={15 * 1024 * 1024}
/>

Muestra los mensajes que guardó tu servidor, y carga los anteriores al subir con el scroll (messages), con el aviso de escritura, las reacciones, las confirmaciones de lectura y el campo para escribir. to, name y avatar ponen a la otra persona en su barra superior. upload acepta fotos: cada una sube a la ruta propia del sitio al elegirla, y el mensaje solo lleva dónde está, así que WordPlus Cloud nunca ve la foto. La ruta pregunta la misma regla que la sala:

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

realtime.mayJoin ejecuta la regla de deal:{id} para la persona que ha iniciado sesión, así que la conversación y sus fotos nunca discrepan sobre quién puede usarlas. La ruta que sirve una foto vuelve a preguntar a mayTalk, y cualquier otra persona recibe un 404, aunque tenga la dirección de la foto.

3. Una llamada para ver el artículo

lib/realtime.ts
realtime.calls('deal:{id}', async (user, other, { id }) => (await mayTalk(user, id)) && (await mayTalk(other, id)));

La llamada lleva el nombre de la conversación, y las dos personas de la conversación pueden llamarse en ella. call en el chat pone los botones de audio y vídeo en su barra superior, y muestra la llamada en el propio chat, junto a los mensajes, así quien compra puede preguntar por un arañazo mientras quien vende gira el tocadiscos hacia la cámara. En otra página del sitio, la llamada sigue en una ventana en la esquina. Una llamada entre dos personas va directa entre sus navegadores cuando puede, por un relay cuando una red lo impide, y está cifrada de extremo a extremo en cada camino (cómo implementar videollamadas en Next.js).

4. Ofertas que dice tu servidor

Una oferta es una promesa de dinero, así que ninguna página puede enviarla. El emit de la sala deja fuera las ofertas, y solo las server actions del sitio crean una, la guardan y avisan a las dos personas:

app/(app)/actions.ts
export async function offer(deal: string, amount: number): Promise<{ error: string } | null> {
    const me = await currentUserId();
    if (!me) return { error: 'Sign in first.' };
    const made = await makeOffer(deal, me, amount);
    if (typeof made === 'string') return { error: made };
    const found = (await dealOf(deal))!;
    await realtime.to(`deal:${deal}`).emit('changed');
    await realtime.toUser(found.listing.seller).emit('offer', { deal, name: found.buyerName, amount, title: found.listing.title });
    return null;
}

realtime.to() dice changed en la sala de la conversación, y sus dos páginas vuelven a pedir su contenido al servidor, sin recargar. realtime.toUser() llega a quien vende en la página en que esté, como un aviso: "Ava offers $135 for Wooden turntable, works perfectly". Accept o Decline vuelve por el mismo camino a quien compra. Cuando se acepta una oferta, el artículo queda reservado a ese precio para quien la hizo, las demás ofertas abiertas sobre él se rechazan, y cada persona que las hizo también se entera:

app/(app)/actions.ts
for (const other of answered.declined) {
    await realtime.toUser(other.buyer).emit('answer', {
        deal: other.deal, name: seller, accepted: false, amount: other.amount, title: answered.listing.title,
    });
}

5. Quién está mirando, y el marketplace tal como está

La página de un artículo entra en una sala propia, listing:7, pública y contada por WordPlus Cloud, sin ninguna tabla tuya:

lib/realtime.ts
realtime.room('listing:{id}', { public: true, count: true });
app/(app)/live.tsx
const { count } = useRoom(`listing:${id}`, { changed: () => router.refresh() });
const others = count === null ? 0 : count - 1;

La página muestra "2 other people are looking at this", y el número cambia según la gente llega y se va. La misma sala oye changed cuando baja el precio, o el artículo se reserva o se vende, y la página lo muestra al instante. Cada página de una persona con sesión iniciada entra en una sala más, market, en la que solo habla el servidor, así que el propio marketplace muestra un artículo nuevo, un precio nuevo o un artículo vendido, sin recargar.

Lo que viene incluido

  • Aviso de escritura y confirmaciones de lectura, en los dos sentidos, como en una app de mensajería.
  • Una bandeja de entrada: cada conversación, comprando y vendiendo, la más reciente primero, y un aviso de cada mensaje nuevo en cualquier página.
  • Si quien vende está en línea, o cuándo se conectó por última vez, en la página del artículo, con useOnline().
  • Nombres y caras de tu app. El chat y la llamada muestran a cada vecino con el nombre y la foto que da tu sitio, sin ninguna lista de usuarios guardada en WordPlus Cloud.
  • Sellado por el camino. Lo que pasa por una sala privada cruza WordPlus Cloud sellado con una clave que deriva tu app, y los servidores de WordPlus Cloud nunca tienen esa clave.

Pruébalo, y luego construye el tuyo

En la demo en vivo, los vecinos y sus cosas son solo tuyos durante un día, así que nadie más ve lo que escribas u ofrezcas allí. Debajo de cada página, How this page works muestra el código que la crea, paso a paso, con los valores de la página que tienes abierta.

La guía del marketplace construye el sitio completo, con cada archivo de su parte en tiempo real, y para reuniones de muchas personas en el mismo tipo de app, lee cómo implementar videoconferencias en Next.js.

WordPlus Cloud for Apps tiene precios por conexiones simultáneas, a partir de 1.000, con facturación mensual o anual. Los mensajes, las ofertas, el recuento de quién está mirando y las llamadas entre dos personas no consumen créditos. Descubre WordPlus Cloud para apps, o consulta los precios para apps. ¿Preguntas sobre tu integración? Escríbenos.