LotZero : zéro ventes en trop et zéro doubles débits sur une enchère mondiale en direct, prouvé sur Aurora DSQL
Le commerce mondial en direct imposait un choix : une base SQL mono-région (juste mais lente pour les enchérisseurs lointains) ou un stockage multi-régions à cohérence éventuelle (rapide mais dangereux pour l'argent). Aurora DSQL fait tomber ce compromis. LotZero pose le registre d'argent sur DSQL et le flux social sur DynamoDB, puis prouve l'invariant avec une console de contention qui déclenche des centaines de réclamations mondiales concurrentes et mesure : zéro ventes en trop, zéro doubles débits.
Le commerce mondial en direct imposait un compromis brutal. Mettre le registre d'argent dans une seule région : juste, mais lent pour tous ceux qui sont loin. L'étaler sur plusieurs régions avec un NoSQL à cohérence éventuelle : rapide, mais dangereux pour l'argent, deux régions peuvent toutes deux vendre la dernière unité, et quelqu'un reçoit un mail de remboursement. Je voulais bâtir la chose réputée impossible sur une stack de week-end : une enchère mondiale en direct où le dernier lot ne peut être gagné qu'une seule fois.

L'idée : Aurora DSQL fait tomber l'ancien compromis
Aurora DSQL offre une cohérence forte multi-régions actif-actif avec des écritures locales à faible latence. La partie qui doit être exactement juste (registre d'enchères, blocages de portefeuille, règlement) devient une app CRUD fortement cohérente ordinaire, et je l'associe à DynamoDB pour la partie qui doit juste être rapide et massive. Cette frontière de cohérence, c'est l'architecture.
- Argent + rareté (Aurora DSQL) : portefeuilles, lots, enchères, registre, règlement. Doit être exactement juste sous contention mondiale. OCC + cohérence forte signifie pas de survente, pas de double débit, pas d'écritures perdues.
- Flux social (DynamoDB) : chat, présence, réactions, classements, fil d'activité. Quasi append-only, des millions d'écritures, latence à un chiffre de ms ; la cohérence éventuelle suffit. Design à table unique.
Les invariants (et comment DSQL les fait tenir)
Toutes les opérations d'argent tournent dans une transaction DSQL fortement cohérente avec contrôle optimiste de concurrence. Les transactions en conflit échouent avec une erreur OCC et sont automatiquement réessayées, re-vérifiant l'état frais. Trois invariants doivent tenir à tout instant :
- L'enchère la plus haute d'un lot ne fait qu'augmenter ; il y a exactement un plus haut enchérisseur.
- qty_claimed ne dépasse jamais qty_total. Zéro survente, partout dans le monde.
- Le « bloqué + dépensé » d'un portefeuille ne dépasse jamais son solde financé. Zéro double débit, même si le même utilisateur agit depuis deux régions dans la même milliseconde.

Modélisation conforme à DSQL
DSQL est compatible PostgreSQL mais délibérément différent. Le schéma respecte tout cela dès le premier jour :
- Pas de clés étrangères (DSQL ne les supporte pas). L'intégrité est tenue à l'intérieur des transactions.
- Pas de séquences ni de SERIAL. Les identifiants sont générés par l'application.
- Pas de colonnes JSON ou JSONB. DSQL ne supporte pas les types JSON.
- Une seule instruction DDL par transaction. Le client applique chaque CREATE TABLE séparément.
- SELECT ... FOR UPDATE est utilisé comme DSQL l'entend : pas un verrou, mais une manière d'enrôler les lignes lues dans la vérification de conflit OCC, de sorte qu'un écrivain en concurrence perd proprement au commit.
- Les index asynchrones sont appliqués hors bande pour que le DDL reste mono-instruction.
L'authentification à DSQL utilise des jetons IAM à durée courte émis par le connecteur officiel aurora-dsql-node-postgres via Vercel OIDC, donc aucun mot de passe statique de base ne traîne dans des variables d'environnement ou dans le dépôt.
La preuve : justesse mesurée sous charge
La page /proof et le script loadtest déclenchent une course mondiale délibérée : de nombreux acheteurs étiquetés sur cinq régions AWS qui se disputent un même lot rare au même instant, et vérifient que l'invariant tient exactement. Deux scénarios :
- Survente : N acheteurs dans le monde se ruent sur un drop avec seulement K unités. Exactement K gagnent, le reste est rejeté proprement, et le vendeur est crédité d'exactement K fois le prix.
- Double débit : un acheteur financé pour un seul achat tente de gagner N lots d'un coup depuis plusieurs régions. Exactement un gagne, et son solde ne devient jamais négatif.
Exécution type : 200 réclamations concurrentes sur un lot rare s'achèvent en moins de 900 ms avec zéro survente et zéro double débit ; 150 tentatives concurrentes sur le scénario de double débit se règlent en environ 690 ms avec exactement un gagnant. Le script sort en code non-zéro si un invariant est jamais violé, donc il sert aussi de garde-fou de CI.

Trois mécaniques d'enchères, un même jeu d'invariants
- Anglaise ascendante : enchère classique au plus offrant. Les fonds sont bloqués sur le plus haut enchérisseur et libérés à l'instant où il se fait surenchérir. Le règlement convertit le blocage en paiement.
- Néerlandaise à prix descendant : le prix baisse sur un horaire ; la première réclamation sur Terre gagne. Le test de contention le plus dur possible, et la démo la plus spectaculaire.
- Drop à prix fixe : un drop mondial à prix fixe et plafond strict (par ex. 50 unités). Prouve l'absence de survente à l'échelle multi-unités.

Ce qui a été difficile
Reformuler FOR UPDATE a été le premier déclic. Sur un Postgres classique, c'est un verrou de ligne. Sur DSQL, c'est un enrôlement OCC : ça ne bloque pas, ça promet juste que la transaction échouera au commit si l'une de ces lignes a été changée par quelqu'un d'autre. Une fois le bon modèle mental en tête, le reste du design suit : modéliser l'intention, pas l'ordre. Ne pas chercher à sérialiser les écrivains ; concevoir pour le cas optimiste et compter sur la boucle de retry pour converger.
Le deuxième a été la perte des clés étrangères et des types JSON. Sans FK, chaque transaction doit valider sa propre intégrité référentielle en ligne. Sans JSON, tout ce qui est dynamique doit prendre la forme de vraies tables. Les deux forcent le schéma à être plus honnête sur ce qu'il est vraiment, ce qui est une bonne pression de design, même en dehors de DSQL.
Le troisième a été de rendre la preuve honnête. Il est facile d'écrire un test de charge qui passe toujours parce qu'il n'y a jamais de vraie contention. La console Proof finance délibérément un portefeuille pour un seul achat, puis dit à N workers concurrents de tous tenter l'achat. Si un portefeuille passe une seule fois négatif, la suite échoue. Cette règle unique (solde jamais négatif) est ce qui rend la page digne d'être montrée.

Parité locale, vraie cluster derrière le même SQL
Sans aucune variable d'environnement, LotZero tourne entièrement en local. Le registre SQL utilise PGlite (Postgres embarqué) qui exécute le SQL identique à celui d'Aurora DSQL, auto-amorcé avec des lots de démo et des portefeuilles financés. Le flux social utilise un stockage en mémoire. On change d'identité et de région d'action depuis l'en-tête, on ouvre un lot, on enchérit. On ouvre le même lot dans deux onglets comme deux utilisateurs dans deux régions pour ressentir la contention.
On définit DSQL_CLUSTER_ENDPOINT pour basculer le registre sur Aurora DSQL, et DYNAMODB_TABLE pour basculer le flux sur DynamoDB. Rien d'autre ne change dans l'app. Même code, même SQL. C'est cette propriété qui a rendu le build serein : chaque commit tournait contre les deux, la suite e2e passait à l'identique, et il n'y avait pas de « chemin de code production » séparé à surveiller.
Impact
Le commerce en direct est le bon terrain. Le livestream shopping aux États-Unis a atteint environ 50 G$ de GMV en 2025 et devrait dépasser 5 % du commerce numérique américain en 2026 ; le marché mondial du live commerce était d'environ 172 G$ en 2025, en croissance d'environ 41 % par an. L'abandon de panier moyen est d'environ 70 %, ce que Baymard estime à environ 260 G$/an de revenu récupérable aux États-Unis seulement. Survendre un drop limité transforme une vente conclue en annulation et en remboursement : exactement le mode d'échec que LotZero rend structurellement impossible. Et la latence mondiale est une taxe mesurable sur la conversion (Amazon : chaque 100 ms coûte environ 1 % des ventes ; Google et Deloitte, Milliseconds Make Millions : une accélération de 0,1 s a fait monter la conversion retail d'environ 8 %). Le SQL mono-région impose cette taxe à chaque acheteur lointain ; les écritures locales actif-actif de DSQL la suppriment tout en restant fortement cohérentes.
Limites honnêtes, chemin vers la production
- Les paiements sont en bac à sable (portefeuilles de démo), pas un vrai PSP. La production demande Stripe ou Adyen, KYC et fiscalité.
- Pas de fournisseur d'authentification ; l'identité est un sélecteur de démo dans l'en-tête. La production demande de vrais comptes et de l'autorisation.
- Anti-fraude, protection contre le sniping d'enchères et gestion des litiges sont hors périmètre pour le build hackathon.
- Le flux social interroge actuellement par polling ; la production utiliserait WebSockets ou SSE et DynamoDB Streams.
- Aurora DSQL a des limites SQL et fonctionnelles documentées ; le schéma respecte les principales (pas de FK, pas de séquences), mais une revue de schéma de production contre l'ensemble actuel des fonctionnalités DSQL est requise.
LotZero: Global Live Auctions With Zero Oversells and Zero Double-Spends on Aurora DSQL
Voir le projet