Blog

Comment créer un chat entre acheteurs et vendeurs pour une marketplace en Next.js

· Andrij Tkachenko · 9 min de lecture

Une fenêtre de navigateur avec la fiche d’un article à gauche, une platine vinyle avec une étiquette de prix et deux boutons pour une offre, et une conversation à droite, des bulles de messages entre deux personnes et une vignette vidéo dans le chat

Un chat entre acheteurs et vendeurs ressemble à une seule fonctionnalité, et dans une marketplace en Next.js il en réunit plusieurs. La personne qui achète pose une question sur un article et reçoit la réponse pendant qu’elle le regarde encore. La conversation n’appartient qu’à ces deux personnes, avec des photos que personne d’autre ne peut ouvrir. Parfois une photo ne suffit pas, et un court appel vidéo montre l’article avant que quiconque se déplace. Une offre doit atteindre la personne qui vend sur n’importe quelle page de l’app, et la réponse doit revenir de la même façon. Et toutes les personnes qui regardent un article devraient voir son prix, et s’il est toujours en vente, dès qu’il change.

Cet article passe en revue chaque partie dont une vraie app a besoin, avec un exemple complet qui fonctionne : la marketplace de notre guide Next.js, où des voisins achètent et vendent leurs affaires. Vous pouvez l’essayer en direct : soyez Omar, qui vend la platine vinyle, puis ouvrez le site dans une fenêtre privée en tant qu’Ava, avec une offre de 130 $ qui attend une réponse. Modifiez l’offre en tant qu’Ava, et Omar l’apprend aussitôt, sur n’importe quelle page.

Ce qu’il faut au chat d’une marketplace

  • Une conversation par article et par acheteur. Chaque personne qui pose une question sur un article a sa propre conversation avec la personne qui le vend, et personne d’autre ne la lit, ni les autres acheteurs ni les autres vendeurs.
  • Des messages qui arrivent pendant qu’on regarde. Un message passe de page en page dès son envoi, avec l’indicateur de saisie et les accusés de lecture, et il est gardé dans votre propre base de données. Les connexions que cela demande restent ouvertes aussi longtemps que la page, et une fonction serverless ne peut pas les garder (pourquoi, et que faire).
  • Des photos gardées chez vous. La personne qui achète veut voir un détail de plus près, et la personne qui vend envoie une photo. Les photos restent sur votre serveur, et seules les deux personnes de la conversation peuvent les ouvrir.
  • Un appel pour voir l’article. En vidéo, depuis la conversation, avant que quiconque se déplace.
  • Des offres qu’aucune page ne peut falsifier. Une offre, et le oui ou le non de la personne qui vend, viennent de votre serveur, jamais d’une page, et les deux côtés les voient tout de suite.
  • La marketplace telle qu’elle est maintenant. Combien d’autres personnes regardent un article, son nouveau prix, et s’il a été réservé ou vendu, sur chaque page qui le montre.

Vous pouvez construire tout cela vous-même, avec vos propres websockets et votre propre WebRTC. Ou vous laissez un cloud temps réel porter les messages, les appels et les comptages, pendant que votre serveur garde ce qui vous appartient vraiment : qui peut parler à qui, les conversations et les offres. C’est ce que fait la marketplace, avec WordPlus Cloud.

L’exemple : une marketplace

Des voisins mettent en vente leurs affaires avec des photos et un prix. La personne qui veut acheter ouvre un article, clique sur Message Omar, et la conversation sur cet article s’ouvre, avec l’article et l’offre au-dessus du chat. C’est une app Next.js avec l’App Router, Better Auth et Postgres, et sa partie temps réel tient en quelques fichiers. Cinq étapes construisent ses conversations.

1. Une conversation par article et par acheteur

Chaque conversation est un salon nommé d’après son numéro dans la base de données, deal:7, et réunit un article, la personne qui le vend et une personne qui l’achète. Le site déclare ces salons une seule fois, avec ce motif, avec ce qu’une page peut y envoyer et avec la règle :

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 demande à la base de données qui sont les deux personnes de la conversation, et ne laisse entrer personne d’autre. Une page demande à entrer sur la route du site lui-même, /api/realtime/auth, qui applique la règle avec la session du site et répond par un laissez-passer signé avec le secret de l’app, ou par la raison du refus. Les messages, les indicateurs de saisie, les réactions et les accusés de lecture passent ensuite de page en page par WordPlus Cloud, et webhook envoie aussi chaque message et chaque réaction au site, qui les garde dans 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 garde le message une seule fois, d’après l’identifiant de son événement, et prévient l’autre personne de la conversation avec realtime.toUser(), qui atteint chaque page ouverte par cette personne : une notification qui ouvre la conversation.

2. Le chat dans la page, avec des photos gardées chez vous

La conversation est un chat prêt à l’emploi, celui de Better Messages, dans votre propre arbre :

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}
/>

Il montre les messages que votre serveur a gardés, et charge les plus anciens quand on remonte (messages), avec l’indicateur de saisie, les réactions, les accusés de lecture et la zone de saisie. to, name et avatar placent l’autre personne dans sa barre du haut. upload accepte des photos : chacune part vers la route du site dès qu’elle est choisie, et le message ne porte que l’endroit où elle se trouve, si bien que WordPlus Cloud ne voit jamais la photo elle-même. La route consulte la même règle que le salon :

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 applique la règle de deal:{id} pour la personne connectée, si bien que la conversation et ses photos ne sont jamais en désaccord sur qui peut s’en servir. La route qui sert une photo redemande à mayTalk, et toute autre personne reçoit une 404, même avec l’adresse de la photo.

3. Un appel pour voir l’article

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

L’appel porte le nom de la conversation, et les deux personnes de la conversation peuvent s’y appeler. call sur le chat place les boutons audio et vidéo dans sa barre du haut, et montre l’appel dans le chat lui-même, à côté des messages : la personne qui achète peut demander d’où vient une rayure pendant que la personne qui vend tourne la platine vers la caméra. Sur une autre page du site, l’appel continue dans une fenêtre dans le coin. Un appel entre deux personnes passe directement d’un navigateur à l’autre quand c’est possible, par un relais quand un réseau l’empêche, et il est chiffré de bout en bout sur chaque chemin (comment implémenter des appels vidéo en Next.js).

4. Des offres dites par votre serveur

Une offre est une promesse d’argent, donc aucune page ne peut en envoyer. L’emit du salon laisse les offres de côté, et seules les server actions du site en créent une, la gardent et préviennent les deux personnes :

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() dit changed dans le salon de la conversation, et ses deux pages redemandent leur contenu au serveur, sans rechargement. realtime.toUser() atteint la personne qui vend sur la page où elle se trouve, sous forme de notification : « Ava offers $135 for Wooden turntable, works perfectly ». Accept ou Decline repart de la même façon vers la personne qui achète. Une fois une offre acceptée, l’article est réservé à ce prix pour la personne qui l’a faite, les autres offres ouvertes sur lui sont refusées, et chaque personne qui en avait fait une l’apprend aussi :

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 regarde, et la marketplace telle qu’elle est

La page d’un article rejoint un salon à elle, listing:7, public et compté par WordPlus Cloud, sans aucune table chez vous :

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 page affiche « 2 other people are looking at this », et le nombre change à mesure que les gens arrivent et repartent. Le même salon entend changed quand le prix baisse, ou quand l’article est réservé ou vendu, et la page le montre aussitôt. Chaque page d’une personne connectée rejoint encore un salon, market, où seul le serveur parle, si bien que la marketplace elle-même montre un nouvel article, un nouveau prix ou un article vendu, sans rechargement.

Ce qui vient avec

  • L’indicateur de saisie et les accusés de lecture, dans les deux sens, comme dans une messagerie.
  • Une boîte de réception : chaque conversation, à l’achat comme à la vente, la plus récente en premier, et une notification de chaque nouveau message sur n’importe quelle page.
  • Si la personne qui vend est en ligne, ou quand elle l’a été pour la dernière fois, sur la page de l’article, avec useOnline().
  • Les noms et les visages de votre app. Le chat et l’appel montrent chaque voisin avec le nom et la photo que donne votre site, sans aucune liste d’utilisateurs gardée dans WordPlus Cloud.
  • Scellé en route. Ce qui passe par un salon privé traverse WordPlus Cloud scellé avec une clé que votre app dérive, et les serveurs de WordPlus Cloud ne détiennent jamais cette clé.

Essayez, puis construisez la vôtre

Dans la démo en direct, les voisins et leurs affaires sont à vous et à personne d’autre pendant une journée, donc ce que vous y écrivez ou proposez, personne d’autre ne le voit. Sous chaque page, How this page works montre le code qui la produit, étape par étape, avec les valeurs de la page que vous avez ouverte.

Le guide de la marketplace construit le site entier, avec chaque fichier de sa partie temps réel, et pour des réunions à plusieurs dans le même genre d’app, lisez comment implémenter la visioconférence en Next.js.

WordPlus Cloud for Apps est tarifé au nombre de connexions simultanées, à partir de 1 000, avec une facturation mensuelle ou annuelle. Les messages, les offres, le comptage de qui regarde et les appels entre deux personnes ne consomment aucun crédit. Découvrez WordPlus Cloud pour les apps, ou consultez les tarifs pour les apps. Des questions sur votre intégration ? Écrivez-nous.