AMSAhub — Place de marché de services freelance

React.jsNode.jsExpressMongoDBStripeREST API· 4 min de lecture

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 User portant un booléen isSeller, embarqué dans la charge utile du JWT à la connexion. getOrders filtre 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.
  • PaymentIntent Stripe créé côté serveur, avec le montant lu depuis l'annonce enregistrée — le navigateur ne reçoit que le clientSecret et n'énonce jamais de prix.
  • Notes dénormalisées sur l'annonce sous forme d'un couple totalStars + starNumber cumulatif : 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.
client (React)httpOnly JWT cookieExpress APIrole read from JWTamount from listingMongoDBlistings · orders ·reviews · messagesStripePaymentIntent

Description complète :

  1. client (React) — httpOnly JWT cookie → Express API — role read from JWT — amount from listing
  2. Express API — role read from JWT — amount from listing → MongoDB — listings · orders · — reviews · messages
  3. Express API — role read from JWT — amount from listing → Stripe — PaymentIntent
Une frontière, deux décisions que le client ne prend jamais : le rôle sous lequel une requête agit est lu dans le JWT, et le montant facturé est lu sur la fiche stockée — le navigateur ne reçoit que le clientSecret.

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 :

Une fiche de service4.08 (12)ce qu'affiche une carte de la grille
Stocké sur le document de la fichetotalStars 49 · starNumber 124.08
Recompté depuis la collection d'avis12 avis4.08

La paire concorde avec la collection — chaque ajout a mis les deux à jour d'un coup.

Ajouter un avis

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.