Une place de marché de services freelance où la même plateforme sert deux intentions opposées : quelqu'un qui vend une compétence et quelqu'un qui en achète une. Les deux côtés parcourent les mêmes annonces, échangent sur les mêmes fils de discussion et lisent les mêmes notes — mais ne veulent presque jamais les voir de la même façon.
Le problème et les contraintes
La difficulté d'une place de marché à deux faces n'est pas le CRUD : c'est que l'acheteur et le vendeur attendent des réponses différentes à partir de données identiques, et qu'il y a de l'argent en jeu. Deux contraintes en découlent : le rôle sous lequel s'exécute une requête ne peut jamais venir du client, et le montant facturé non plus. Une troisième vient des pages d'annonces — une grille de cartes affichant des notes en étoiles ne peut pas se permettre d'agréger une collection d'avis à chaque rendu.
Approche
- Un seul modèle
Userportant un booléenisSeller, embarqué dans la charge utile du JWT à la connexion.getOrdersfiltre alors sur le rôle lu dans le jeton — un seul endpoint, deux réponses justes, et aucun paramètre de rôle fourni par le client à falsifier. - Le JWT vit dans un cookie httpOnly plutôt que dans
localStorage: le script côté navigateur ne peut pas le lire. PaymentIntentStripe créé côté serveur, avec le montant lu depuis l'annonce enregistrée — le navigateur ne reçoit que leclientSecretet n'énonce jamais de prix.- Notes dénormalisées sur l'annonce sous forme d'un couple
totalStars+starNumbercumulatif : une carte calcule sa moyenne à partir de deux entiers qu'elle possède déjà. - Conversations et messages modélisés en collections séparées, ce qui garde le chargement de la liste des fils indépendant du volume de messages qu'ils contiennent.
Description complète :
- client (React) — httpOnly JWT cookie → Express API — role read from JWT — amount from listing
- Express API — role read from JWT — amount from listing → MongoDB — listings · orders · — reviews · messages
- Express API — role read from JWT — amount from listing → Stripe — PaymentIntent
Compromis faits
Decision
Une note dénormalisée sur l'annonce plutôt qu'une agrégation des avis à la lecture : deux entiers sur le document que la carte récupère déjà, au lieu d'une jointure par carte sur une grille — en acceptant que le couple doive être mis à jour de façon transactionnelle à chaque nouvel avis, et qu'un avis supprimé exige une correction explicite au lieu de disparaître d'un simple recomptage.
Decision
Le rôle porté par le jeton plutôt que relu en base à chaque requête : supprime une lecture sur chaque appel authentifié, au prix d'un rôle figé pour la durée de vie du jeton — un vendeur qui change de statut reste sur l'ancienne vue jusqu'à réémission.
Le premier de ces deux compromis porte son coût dans une subordonnée : un avis supprimé doit être corrigé explicitement. Voici cette paire en direct — ajoutez des avis, elle reste juste ; supprimez-en un, regardez-la décrocher :
La paire concorde avec la collection — chaque ajout a mis les deux à jour d'un coup.
Documents lus pour rendre cette grille : 24 avec la paire stockée · 312 en recomptant par fiche
Démonstration du modèle de données, calculée en direct dans votre navigateur — la même mise à jour à deux entiers que fait la marketplace. Ni vraies fiches, ni vrais avis, ni temps de requête mesurés.
Résultats
Une boucle de place de marché fonctionnelle : publier une annonce, en discuter, payer via Stripe, laisser un avis — avec les décisions d'autorisation et de tarification prises côté serveur plutôt que confiées au client.
Ce que je referais différemment
Déplacer la création de commande vers un webhook Stripe. En l'état, la
ligne de commande est écrite à la création du PaymentIntent, avant la
confirmation du paiement : un panier abandonné laisse donc une commande
persistée que seul le filtre isCompleted masque — la donnée reste
honnête mais la collection n'est pas propre, et c'est le rappel du
prestataire de paiement qui devrait faire foi. Je sortirais aussi la
devise usd codée en dur vers la configuration.