Un client m'a appelé un jeudi après-midi en 2021, antiquaire basé à Bath, qui voulait mettre ses enchères mensuelles en personne en ligne. Assez simple, me suis-je dit. Puis il a dit « et les enchères doivent se mettre à jour en direct pour tout le monde qui regarde, sans actualiser la page ». Ah bon. C'est à ce moment-là qu'un « simple travail WordPress » s'est transformé en une conversation d'architecture de deux semaines.
Point clé : Les enchères en direct sur Next.js plus Supabase reposent sur les canaux temps réel, la sécurité au niveau des lignes et la validation des enchères côté serveur ; maîtrisez la machine d'état avant l'interface utilisateur.
J'ai construit plus de 12 000 sites chez Seahawk Media, et les fonctionnalités en temps réel sont celles qui vous causent des problèmes si vous ne les planifiez pas correctement dès le départ. L'interrogation toutes les cinq secondes semble correcte jusqu'à ce que vous ayez 200 enchérisseurs frappant un seul point de terminaison simultanément et que votre facture d'hébergement double du jour au lendemain. Alors laissez-moi vous montrer exactement comment je construirais une plateforme d'enchères en direct appropriée aujourd'hui, en utilisant Next.js et Supabase, basée sur ce que j'ai réellement déployé.
---
Pourquoi Next.js et Supabase pour cela spécifiquement
Écoutez, il y a une douzaine de façons de faire du temps réel. Socket.io sur un serveur Node, Ably, Pusher, Firebase, j'ai utilisé tous ces outils à différents moments. Mais la combinaison Next.js + Supabase mérite sa place ici pour une raison spécifique : Supabase Realtime est construit sur la réplication logique de PostgreSQL, ce qui signifie que vos mises à jour d'enchères en direct et votre couche de données persistantes sont le même système. Pas de synchronisation de deux sources de vérité. Pas de doute sur le fait qu'une enchère qui est entrée dans le WebSocket est aussi entrée dans la base de données.
Supabase vous donne aussi l'authentification, la sécurité au niveau des lignes (RLS) et le stockage d'emblée. Pour un site d'enchères, où « seul le propriétaire de l'enchère peut clôturer un lot » et « un utilisateur ne peut pas enchérir sur son propre article » sont de véritables règles métier, les politiques RLS dans Postgres sont vraiment l'outil qu'il faut.
Et Next.js parce que, honnêtement, l'App Router avec les Server Components signifie que vous pouvez rendre le catalogue des enchères de manière statique, garder le SEO heureux, et hydrater uniquement le widget d'enchères en temps réel sur le client. Cette séparation compte. Vous ne voulez pas payer pour un rendu dynamique sur une page qui contient 90 % de contenu statique.
---
Concevoir le schéma en premier (Ne le sautez pas)
C'est là que la plupart des gens se précipitent et le regrettent plus tard. J'ai passé trois jours embarrassants à refactoriser le schéma du client Bath antiques en plein projet parce que je n'avais pas bien réfléchi au modèle d'historique des enchères.
Voici la structure fondamentale que j'utilise maintenant :
- `profiles`, étend auth.users de Supabase, stocke le nom d'affichage, l'indicateur d'enchérisseur vérifié, et un solde_crédit si vous faites des enchères à base de dépôt
- `auctions`, l'événement lui-même ; starts_at, ends_at, status (draft | live | closed), et created_by
- `lots`, articles individuels au sein d'une enchère ; reserve_price, current_bid, current_bidder_id, lot_number, ends_at (les lots peuvent avoir des comptes à rebours individuels)
- `bids`, journal immuable en ajout seul ; lot_id, bidder_id, amount, placed_at. Ne mettez jamais à jour ce tableau. Jamais.
- `auction_participants`, une table de jonction suivant qui s'est inscrit pour quelle enchère (utile pour les blocs de dépôt et l'orientation des notifications)
Les colonnes current_bid et current_bidder_id sur les lots sont dénormalisées intentionnellement. Oui, vous pourriez les dériver de la table bids à chaque lecture, mais sous une charge concurrente cette requête devient chère rapidement. Dénormalisez, gardez la table bids comme votre journal d'audit, et utilisez une fonction Postgres pour mettre à jour les lots atomiquement quand une enchère est acceptée.
La fonction d'enchère atomique
C'est la partie que la plupart des tutoriels omettent. Les conditions de concurrence dans les enchères sont réelles. Deux utilisateurs soumettent £520 à la même milliseconde, qu'est-ce qui se passe ?
La réponse est une fonction Postgres avec verrouillage FOR UPDATE sur la ligne du lot :
``` create or replace function place_bid(p_lot_id uuid, p_bidder_id uuid, p_amount numeric) returns json as $$ declare v_lot lots%rowtype; begin select * into v_lot from lots where id = p_lot_id for update;
if v_lot.status != 'live' then return json_build_object('success', false, 'error', 'Lot is not live'); end if;
if p_amount <= v_lot.current_bid then return json_build_object('success', false, 'error', 'Bid too low'); end if;
if p_bidder_id = v_lot.current_bidder_id then return json_build_object('success', false, 'error', 'You are already the highest bidder'); end if;
insert into bids (lot_id, bidder_id, amount) values (p_lot_id, p_bidder_id, p_amount);
update lots set current_bid = p_amount, current_bidder_id = p_bidder_id where id = p_lot_id;
return json_build_object('success', true, 'new_bid', p_amount); end; $$ language plpgsql security definer; ```
Appelez ceci depuis votre route API Next.js via supabase.rpc('place_bid', {...}). Le verrou FOR UPDATE signifie qu'une seule transaction gagne par lot à un moment donné. L'autre reçoit une erreur de sérialisation et vous retournez un message amical « quelqu'un vient de vous surenchérir » sur le client.
---
Row Level Security, la couche des règles d'enchères
RLS est l'une de ces choses que les développeurs adorent immédiatement ou évitent parce que ça semble opaque. J'étais dans le camp de l'évitement jusqu'à ce qu'un projet fintech chez Seahawk m'enseigne à la dure que forcer le contrôle d'accès uniquement dans le code d'application est à une route API mal configurée près du désastre.
Pour un site d'enchères, voici les politiques qui importent :
- N'importe qui peut lire les lots actifs, SELECT sur lots où auctions.status = 'live'
- Seuls les enchérisseurs authentifiés et vérifiés peuvent insérer des enchères, vérifiez profiles.verified_bidder = true dans la politique
- Seul le créateur de l'enchère peut mettre à jour le statut du lot, UPDATE sur lots où auctions.created_by = auth.uid()
- L'historique des enchères est lisible par le créateur de l'enchère du lot et l'enchérisseur lui-même, personne d'autre n'a besoin de voir l'historique complet des enchères en temps réel
La documentation Supabase RLS est vraiment bonne ici, cela vaut la peine de lire la section sur les fonctions security definer, car elle interagit avec le fonctionnement des appels RPC comme place_bid.
Un piège : si vous utilisez security definer sur votre fonction Postgres (comme ci-dessus), elle s'exécute avec les privilèges du propriétaire de la fonction, contournant RLS. C'est intentionnel, vous voulez que le placement de l'enchère contourne RLS de l'enchérisseur pour pouvoir verrouiller et mettre à jour la ligne du lot. Mais cela signifie que vous devez appliquer vos propres vérifications de logique métier à l'intérieur de la fonction, ce que le code ci-dessus fait.
---
Configuration de Supabase Realtime dans Next.js
C'est là que ça devient vraiment satisfaisant. Supabase Realtime vous permet de vous abonner aux modifications d'une table Postgres en utilisant WebSockets en arrière-plan, et le SDK client le rend presque honteusement simple.
Sur votre page de lot d'enchères, un Client Component dans Next.js App Router, vous feriez quelque chose comme :
``` 'use client'
import { useEffect, useState } from 'react' import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
export default function LotBidDisplay({ lotId, initialBid }) { const [currentBid, setCurrentBid] = useState(initialBid) const supabase = createClientComponentClient()
useEffect(() => { const channel = supabase.channel(lot-${lotId}).on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'lots', filter:id=eq.${lotId}}, (payload) => { setCurrentBid(payload.new.current_bid) } ).subscribe()
return () => { supabase.removeChannel(channel) } }, [lotId])
return <div>Current bid: £{currentBid.toLocaleString()}</div> } ```
Passez initialBid depuis un Server Component qui récupère les données fraîches au moment de la requête. Le client prend ensuite le relais, en écoutant les événements UPDATE sur cette ligne de lot spécifique. Chaque fois que place_bid s'exécute avec succès, Supabase diffuse le changement et l'interface utilisateur de chaque enchérisseur connecté se met à jour en environ 100-300ms généralement.
Gérer le minuteur de compte à rebours
Les lots ont généralement un compte à rebours, « ferme dans 3:42 ». Ne faites pas confiance à l'horloge du client pour cela. Dérivez l'heure de fin de lots.ends_at (stockée en UTC dans Postgres) et calculez les secondes restantes sur le client en utilisant Date.now(). Resynchronisez toutes les 60 secondes avec une nouvelle requête en cas de dérive. Et ajoutez la logique de « fermeture douce » : si une enchère arrive dans les 60 dernières secondes, prolongez ends_at de deux minutes. C'est le comportement standard des enchères et les enchérisseurs s'y attendent.
---
Architecture Next.js App Router pour l'interface d'enchères
La structure de page que j'utiliserais :
``app/ auctions/ page.tsx ← Server Component, liste les enchères en direct (ISR, revalidate: 60) [auctionId]/ page.tsx ← Server Component, récupère la liste des lots côté serveur LotGrid.tsx ← Client Component, s'abonne aux changements de statut des lots [lotId]/ page.tsx ← Server Component, données initiales du lot + métadonnées pour le SEO BidPanel.tsx ← Client Component, affichage des enchères en temps réel + formulaire d'enchère``
Le catalogue (/auctions) utilise l'Incremental Static Regeneration avec une revalidation de 60 secondes. Les pages de lots individuels se rendent côté serveur au premier chargement (pour le partage, l'aperçu, la génération og:image), puis sont confiées aux composants client pour les mises à jour en direct.
Une chose que je fais toujours : garder le composant BidPanel en lazy-loading derrière dynamic(() => import('./BidPanel'), { ssr: false }). Ça n'a de sens que côté client de toute façon, et ça garde votre payload HTML initial léger pour les utilisateurs sur connexions lentes, ce qui, si votre audience d'enchères penche vers l'âge (comme c'est le cas pour les ventes aux enchères d'antiquités), compte plus que vous ne l'imaginez.
---
L'authentification et le flux « Enchérisseur Vérifié »
L'authentification standard Supabase avec email/mot de passe ou lien magique fonctionne bien pour l'inscription. Mais les enchères nécessitent souvent une étape supplémentaire : la vérification de l'enchérisseur. Vous pourriez avoir besoin d'un blocage de carte de crédit, d'une vérification d'identité, ou simplement d'une approbation administrateur avant que quelqu'un puisse réellement placer une enchère.
Le modèle que j'utilise : un booléen verified_bidder sur la table des profils, défini par défaut à false. Après l'inscription, l'utilisateur voit un écran « Complétez votre enregistrement ». Une fois approuvé (manuellement par l'administrateur, ou automatiquement après une autorisation de paiement Stripe), vous basculez l'indicateur. La politique RLS sur les enchères la vérifie. Ils peuvent parcourir, suivre, mais pas enchérir jusqu'à vérification.
Pour les autorisations de paiement Stripe, les payment intents de Stripe avec capture_method: manual c'est la bonne approche : vous autorisez une retenue de 50 £, vous la capturez s'ils gagnent, vous la relâchez s'ils ne gagnent pas. Ça réduit drastiquement les situations de non-paiement qui, croyez-moi, sont le cauchemar de chaque opérateur de vente aux enchères en ligne.
---
Déploiement, Performance et les Points qui vous Mordront
Déployez sur Vercel, c'est le choix évident pour Next.js et le réseau edge fonctionne bien avec l'infrastructure mondiale de Supabase. Assurez-vous que votre projet Supabase est dans la région AWS la plus proche de votre région de déploiement Vercel. J'ai vu 40-60 ms de latence complètement inutile parce que quelqu'un avait déployé Vercel en us-east-1 et Supabase en eu-west-2. Choisissez une région, mettez les deux là.
Quelques points qui vous causeront des problèmes si vous ne les traitez pas en amont :
- Limites de connexion WebSocket. Le niveau gratuit de Supabase permet environ 200 connexions Realtime simultanées. Si votre enchère devient virale, ce plafond compte. Vérifiez votre plan.
- Interface utilisateur optimiste pour les enchères. Affichez l'enchère immédiatement sur l'écran de l'enchérisseur avant que le serveur ne la confirme. Si elle échoue (surenchère, condition de concurrence), annulez avec une erreur. L'aller-retour serveur de 200-300 ms est imperceptible sauf si l'interface utilisateur l'attend.
- Période de grâce de fermeture du lot. Ne fermez jamais un lot exactement à ends_at. Donnez-lui un tampon côté serveur de 2-3 secondes pour permettre aux enchères en vol qui ont été soumises juste avant la limite de temps de traiter. Gérez cela dans votre fonction planifiée close_lot.
- Notifications par email. Utilisez Supabase Edge Functions avec Resend ou Postmark pour envoyer les emails "Vous avez été surenchéri" et "Vous avez gagné !". N'essayez pas de faire ça depuis vos routes API Next.js, elles peuvent time out, et les participants aux enchères s'énervent vraiment si les notifications sont peu fiables.
---
FAQ
Combien d'enchérisseurs simultanés Supabase Realtime peut-il gérer ?
Le plan Pro de Supabase supporte jusqu'à 500 connexions Realtime simultanées par défaut, avec des limites plus élevées disponibles. Pour la plupart des sites d'enchères, à moins que vous ne fassiez tourner quelque chose de la taille de Sotheby's en ligne, c'est plus que suffisant. Si vous attendez des milliers de spectateurs simultanés, envisagez de diffuser les mises à jour des lots par un seul canal côté serveur plutôt que par des souscriptions par utilisateur, et regardez la fonctionnalité Supabase Realtime Broadcast qui est plus efficace pour les scénarios de fan-out élevé.
Dois-je utiliser Supabase Realtime ou un service dédié comme Ably ?
Pour la plupart des projets, Supabase Realtime est tout à fait adéquat et l'intégration est beaucoup plus simple puisque vos données sont déjà dans Supabase. Je n'irais chercher Ably ou Pusher que si vous avez besoin d'une latence inférieure à 50 ms mondialement, ou si vous construisez quelque chose avec des millions de connexions simultanées. Une vente aux enchères d'antiquités, une levée de fonds caritative, la vente en ligne d'une petite galerie d'art, Supabase gère tout ça sans problème.
Que se passe-t-il si la connexion WebSocket d'un utilisateur se coupe au milieu de la vente aux enchères ?
Le SDK client de Supabase tentera de se reconnecter automatiquement. Mais tu devrais toujours récupérer l'état actuel du lot (current_bid, ends_at) lors de la reconnexion plutôt que de faire confiance à ce qui était dans l'état local avant la déconnexion. Ajoute un écouteur d'événement online/offline dans ton Client Component et déclenche une récupération serveur fraîche quand la connexion se rétablit.
Puis-je utiliser les Server Actions Next.js pour placer des enchères au lieu d'une route API ?
Oui, et je l'ai fait. Les Server Actions dans Next.js 14 sont pratiques, elles éliminent la boîte à outils d'une route /api/bid dédiée. Le compromis c'est que les Server Actions sont un peu plus difficiles à rate-limit individuellement (vous appliqueriez le rate limiting au niveau du middleware plutôt que par action). Pour un site d'enchères en production, j'ajouterais du rate limiting Upstash Redis en middleware pour empêcher un utilisateur unique de spammer des requêtes d'enchères, que vous utilisiez les Actions ou les routes API.
Comment gérez-vous les égalités, deux enchères identiques au même moment ?
Le verrou FOR UPDATE dans la fonction Postgres place_bid sérialise les enchères simultanées, donc techniquement les égalités ne peuvent pas survenir au niveau de la base de données. L'une réussira, l'autre échouera avec une réponse « enchère trop faible » (puisque les deux égalent current_bid et la vérification est p_amount <= v_lot.current_bid). Premier arrivé, premier servi. C'est la pratique standard des enchères et la plupart des enchérisseurs la comprennent.
---
Le marchand d'antiquités de Bath, pour ce que ça vaut, fait fonctionner ses enchères mensuelles en ligne depuis plus de deux ans maintenant. Le pic d'enchérisseurs simultanés un samedi soir a atteint 84, tout son village apparemment en train de regarder un lot de silver géorgien disputé se vendre trois fois sa mise à prix. Supabase n'a pas bronché. Next.js n'a pas bronché. La seule chose qui a cassé c'était son Wi-Fi, parce qu'il le faisait tourner depuis le plancher du magasin.
Le temps réel c'est difficile à penser à l'avance, mais une fois que le schéma est solide et la fonction d'enchère atomique en place, le reste c'est surtout de la tuyauterie. Faites fondations correctement et vous passerez votre temps sur les bonnes parties, les animations de compte à rebours, l'UX "une fois, deux fois", plutôt que de déboguer les race conditions à minuit.
