Supabase Realtime repose sur le Write-Ahead Log (WAL) de PostgreSQL et utilise un serveur basé sur Elixir pour transmettre les modifications de la base de données via WebSockets aux clients connectés. Pour un système d'enchères, cela signifie que chaque enchère qui frappe ta base de données se propage instantanément à chaque enchérisseur qui regarde cette enchère. Pas d'interrogation. Pas d'infrastructure pub/sub distincte. Ta base de données est ton système d'événements.

Construisons-en un à partir de zéro.

Table des matières

Aperçu de l'architecture

Avant d'écrire du code, comprenons ce que nous construisons et comment les éléments s'assemblent.

Supabase Realtime te donne trois primitives qui s'alignent parfaitement sur les exigences d'enchères :

  • Postgres Changes : S'abonner aux événements INSERT, UPDATE et DELETE sur tes tables d'enchères et d'enchères. Quand quelqu'un place une enchère, chaque abonné reçoit les nouvelles données de ligne en millisecondes.
  • Broadcast : Envoyer des messages éphémères aux participants du canal. Parfait pour les notifications « tu as été surenchéri » qui n'ont pas besoin d'être persistées.
  • Presence : Suivre qui regarde actuellement une enchère. Cela te permet d'afficher « 14 enchérisseurs regardent » dans ton interface utilisateur et de détecter les sessions fantômes.

Le flux de données ressemble à ceci :

  1. L'enchérisseur soumet une enchère via ton frontend
  2. Un appel RPC ou une insertion directe frappe ta table bids
  3. Un déclencheur PostgreSQL valide le montant de l'enchère et met à jour auctions.current_high_bid
  4. Supabase Realtime récupère le changement WAL et le pousse vers tous les abonnés du canal de l'enchère
  5. Un deuxième déclencheur déclenche un événement Broadcast pour notifier le précédent enchérisseur le plus élevé qu'il a été surenchéri
  6. Chaque client connecté met à jour son interface en temps réel

La latence entre le placement de l'enchère et la mise à jour de l'interface utilisateur sur tous les clients est généralement inférieure à 100 ms. J'ai mesuré p99 à environ 80-90 ms en production sur le niveau Pro de Supabase.

Pourquoi ne pas simplement utiliser l'interrogation ?

Je sais que certains d'entre vous pensent « ne puis-je pas interroger toutes les 500 ms ? ». Tu peux. Mais avec 200 enchérisseurs simultanés sur une seule enchère, ce sont 400 requêtes par seconde frappant ta base de données pour une enchère. Multiplie cela par 50 enchères actives et tu atteins 20 000 requêtes par seconde — dont la plupart ne retournent rien de nouveau. Les WebSockets retournent ce modèle : zéro requête quand rien ne change, mises à jour instantanées quand quelque chose change.

Schéma de base de données et configuration

Voici le schéma que j'utilise. C'est délibérément simple — tu peux l'étendre, mais la structure de base gère la plupart des types d'enchères.

-- Table des enchères
CREATE TABLE auctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  item_name TEXT NOT NULL,
  description TEXT,
  starting_price DECIMAL(12,2) NOT NULL DEFAULT 0,
  current_high_bid DECIMAL(12,2) DEFAULT 0,
  highest_bidder_id UUID REFERENCES auth.users(id),
  min_increment DECIMAL(12,2) DEFAULT 1.00,
  status TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('scheduled', 'active', 'ended', 'sold', 'cancelled')),
  starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 minutes',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Table des enchères
CREATE TABLE bids (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  auction_id UUID NOT NULL REFERENCES auctions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  amount DECIMAL(12,2) NOT NULL,
  placed_at TIMESTAMPTZ DEFAULT NOW(),
  CONSTRAINT positive_amount CHECK (amount > 0)
);

-- Index pour les recherches d'enchères rapides par enchère
CREATE INDEX idx_bids_auction_amount ON bids(auction_id, amount DESC);
CREATE INDEX idx_bids_auction_time ON bids(auction_id, placed_at DESC);

-- Critique : activer l'identité de réplica pour Realtime
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;

Le paramètre REPLICA IDENTITY FULL est essentiel. Sans lui, Supabase Realtime n'obtient que la clé primaire sur les événements UPDATE et DELETE — pas les données de ligne complètes. Pour un système d'enchères, tu as besoin de la charge complète afin que les clients puissent mettre à jour les montants d'enchères sans faire une requête distincte.

Activation de la réplication Realtime

Dans le tableau de bord Supabase, allez à Database → Replication et activez la réplication pour les deux tables auctions et bids. Alternativement, tu peux faire cela avec SQL :

BEGIN;
  -- Supprimer la publication existante si elle existe
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- Créer la publication avec les deux tables
  CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;

Sécurité au niveau des lignes

Ne saute pas cela. RLS est ta couche de validation côté serveur.

ALTER TABLE auctions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;

-- Tout le monde peut voir les enchères actives
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- Les utilisateurs authentifiés peuvent voir toutes les enchères sur les enchères actives
CREATE POLICY "View bids on active auctions" ON bids
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = bids.auction_id
      AND auctions.status = 'active'
    )
  );

-- Seuls les utilisateurs authentifiés peuvent placer des enchères
CREATE POLICY "Place bids" ON bids
  FOR INSERT WITH CHECK (
    auth.uid() = user_id
    AND EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = auction_id
      AND auctions.status = 'active'
      AND auctions.ends_at > NOW()
    )
  );

Déclencheurs PostgreSQL pour la logique d'enchères

C'est là que la vraie magie se produit. La base de données applique toute la logique d'enchères côté serveur — le client ne peut pas tricher.

Déclencheur de validation des enchères et de mise à jour de l'enchère

CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
  v_auction auctions%ROWTYPE;
BEGIN
  -- Verrouiller la ligne de l'enchère pour prévenir les conditions de course
  SELECT * INTO v_auction
  FROM auctions
  WHERE id = NEW.auction_id
  FOR UPDATE;

  -- Valider que l'enchère est active
  IF v_auction.status != 'active' THEN
    RAISE EXCEPTION 'Auction is not active';
  END IF;

  -- Valider que l'enchère n'a pas pris fin
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- Valider que le montant de l'enchère dépasse la note actuelle + incrément minimum
  IF NEW.amount < v_auction.current_high_bid + v_auction.min_increment THEN
    RAISE EXCEPTION 'Bid must be at least % higher than current high bid of %',
      v_auction.min_increment, v_auction.current_high_bid;
  END IF;

  -- Empêcher l'auto-surenchère
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- Mettre à jour l'enchère avec la nouvelle enchère la plus élevée
  UPDATE auctions
  SET
    current_high_bid = NEW.amount,
    highest_bidder_id = NEW.user_id,
    updated_at = NOW()
  WHERE id = NEW.auction_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER validate_and_process_bid
  BEFORE INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION process_new_bid();

Ce verrou FOR UPDATE sur la ligne de l'enchère est critique. Sans lui, deux enchères arrivant simultanément pourraient toutes deux lire le même current_high_bid, passer toutes deux la validation et être toutes deux insérées. Le verrou sérialise l'accès.

Notifications de surenchère diffusées

Ce déclencheur se déclenche après une enchère réussie et envoie une notification éphémère au canal de l'enchère :

CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
  v_previous_bidder UUID;
BEGIN
  -- Trouver qui vient d'être surenchéri
  SELECT user_id INTO v_previous_bidder
  FROM bids
  WHERE auction_id = NEW.auction_id
    AND id != NEW.id
  ORDER BY amount DESC
  LIMIT 1;

  -- Diffuser une notification de surenchère s'il y avait un enchérisseur antérieur
  IF v_previous_bidder IS NOT NULL THEN
    PERFORM realtime.send(
      jsonb_build_object(
        'auction_id', NEW.auction_id,
        'new_high', NEW.amount,
        'outbid_user', v_previous_bidder,
        'new_leader', NEW.user_id
      ),
      'outbid',
      'auction:' || NEW.auction_id::text,
      true
    );
  END IF;

  RETURN NULL;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER after_bid_notify
  AFTER INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION notify_outbid();

Souscription côté client avec JavaScript

Maintenant, filons l'avant. Je montrerai ceci avec les patterns vanille JavaScript/React — la même approche fonctionne si tu construis avec Next.js ou tout autre framework.

Initialiser le client

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  {
    realtime: {
      params: {
        eventsPerSecond: 20 // Ralentir le trafic d'enchères
      }
    }
  }
);

Ce paramètre eventsPerSecond compte. Sur une enchère chaude avec des dizaines d'enchères par seconde, tu ne veux pas te restituer 50 fois par seconde. Vingt mises à jour par seconde, c'est plus que suffisant pour une interface utilisateur fluide.

S'abonner à un canal d'enchère

function subscribeToAuction(auctionId, callbacks) {
  const channel = supabase.channel(`auction:${auctionId}`);

  channel
    // Écouter les nouvelles enchères via Postgres Changes
    .on('postgres_changes', {
      event: 'INSERT',
      schema: 'public',
      table: 'bids',
      filter: `auction_id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onNewBid(payload.new);
    })

    // Écouter les mises à jour du statut de l'enchère
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // Écouter les notifications de surenchère diffusées
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // Suivre les enchérisseurs actifs via Presence
    .on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState();
      const bidderCount = Object.keys(state).length;
      callbacks.onPresenceUpdate(bidderCount, state);
    })

    .subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        // Suivre la présence de cet utilisateur
        await channel.track({
          user_id: supabase.auth.getUser()?.data?.user?.id,
          status: 'watching',
          joined_at: new Date().toISOString()
        });
      }
    });

  return channel;
}

React Hook pour les abonnements aux enchères

import { useState, useEffect, useCallback } from 'react';

function useAuction(auctionId) {
  const [auction, setAuction] = useState(null);
  const [bids, setBids] = useState([]);
  const [bidderCount, setBidderCount] = useState(0);
  const [isOutbid, setIsOutbid] = useState(false);

  useEffect(() => {
    // Récupérer l'état initial
    async function loadAuction() {
      const { data: auctionData } = await supabase
        .from('auctions')
        .select('*')
        .eq('id', auctionId)
        .single();
      setAuction(auctionData);

      const { data: bidData } = await supabase
        .from('bids')
        .select('*')
        .eq('auction_id', auctionId)
        .order('amount', { ascending: false })
        .limit(20);
      setBids(bidData || []);
    }
    loadAuction();

    // S'abonner aux mises à jour en temps réel
    const channel = subscribeToAuction(auctionId, {
      onNewBid: (bid) => {
        setBids(prev => [bid, ...prev].slice(0, 20));
        setIsOutbid(false);
      },
      onAuctionUpdate: (updated) => setAuction(updated),
      onOutbid: (payload) => {
        const currentUser = supabase.auth.getUser()?.data?.user;
        if (payload.outbid_user === currentUser?.id) {
          setIsOutbid(true);
        }
      },
      onPresenceUpdate: (count) => setBidderCount(count)
    });

    return () => {
      supabase.removeChannel(channel);
    };
  }, [auctionId]);

  const placeBid = useCallback(async (amount) => {
    const user = (await supabase.auth.getUser()).data.user;
    const { data, error } = await supabase
      .from('bids')
      .insert({
        auction_id: auctionId,
        amount: parseFloat(amount),
        user_id: user.id
      })
      .select()
      .single();

    if (error) throw new Error(error.message);
    return data;
  }, [auctionId]);

  return { auction, bids, bidderCount, isOutbid, placeBid };
}

Gestion des conditions de course et validation des enchères

Les conditions de course sont la source unique la plus importante de bugs dans les systèmes d'enchères. Voici comment je les gère.

Côté serveur : PostgreSQL effectue le gros du travail

Le SELECT ... FOR UPDATE dans notre fonction de déclenchement est la première ligne de défense. Mais il y a un autre modèle que j'ai commencé à utiliser — les verrous consultatifs pour les enchères à contention élevée :

CREATE OR REPLACE FUNCTION place_bid_safe(
  p_auction_id UUID,
  p_user_id UUID,
  p_amount DECIMAL
)
RETURNS TABLE(bid_id UUID, new_high DECIMAL) AS $$
DECLARE
  v_lock_key BIGINT;
  v_bid_id UUID;
BEGIN
  -- Générer une clé de verrou déterministe à partir de l'UUID de l'enchère
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- Acquérir un verrou consultatif (bloque les enchères simultanées sur la même enchère)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- Maintenant en sécurité pour insérer (le déclencheur gère la validation)
  INSERT INTO bids (auction_id, user_id, amount)
  VALUES (p_auction_id, p_user_id, p_amount)
  RETURNING id INTO v_bid_id;

  RETURN QUERY
  SELECT v_bid_id, p_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Appelle ceci à partir du client en utilisant l'RPC de Supabase :

const { data, error } = await supabase.rpc('place_bid_safe', {
  p_auction_id: auctionId,
  p_user_id: user.id,
  p_amount: bidAmount
});

Côté client : interface utilisateur optimiste avec restauration

Montre l'enchère immédiatement dans l'interface utilisateur, mais sois prêt à la restaurer si le serveur la rejette :

async function handleBidSubmit(amount) {
  const optimisticBid = {
    id: crypto.randomUUID(),
    amount,
    user_id: user.id,
    placed_at: new Date().toISOString(),
    _optimistic: true
  };

  // Afficher immédiatement
  setBids(prev => [optimisticBid, ...prev]);

  try {
    await placeBid(amount);
    // L'enchère réelle arrivera via Realtime et remplacera celle optimiste
  } catch (err) {
    // Supprimer l'enchère optimiste en cas d'échec
    setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
    showError(err.message);
  }
}

Suivi de la présence pour les enchérisseurs actifs

Afficher combien de personnes regardent une enchère crée de l'urgence. Le suivi de la présence est mort simple avec Supabase :

// Mettre à jour le statut de l'utilisateur quand il commence à enchérir
async function updatePresenceStatus(channel, status) {
  await channel.track({
    user_id: user.id,
    status, // 'watching', 'bidding', 'won'
    last_active: new Date().toISOString()
  });
}

Du côté de l'affichage, tu peux décomposer l'état de présence pour montrer combien sont activement en train d'enchérir par rapport à juste regarder :

function parseBidderStats(presenceState) {
  const users = Object.values(presenceState).flat();
  return {
    total: users.length,
    bidding: users.filter(u => u.status === 'bidding').length,
    watching: users.filter(u => u.status === 'watching').length
  };
}

Optimisation des performances et considérations de production

Ralentissement et rebondissement

Une guerre d'enchères peut générer des dizaines d'événements par seconde. Voici ce que je configure :

  • Côté serveur : eventsPerSecond: 20 sur la configuration du client Supabase
  • Côté client : Rebondir le bouton d'enchère à 300ms pour prévenir les clics doubles
  • Mises à jour de l'interface : Utiliser requestAnimationFrame pour les animations de la liste des enchères

Synchronisation de la fin de l'enchère

Ne fais pas confiance à l'horloge du client. Utilise un travail PostgreSQL cron via pg_cron :

-- Exécuter toutes les 10 secondes pour fermer les enchères expirées
SELECT cron.schedule(
  'close-expired-auctions',
  '*/10 * * * * *',
  $$
  UPDATE auctions
  SET status = CASE
    WHEN highest_bidder_id IS NOT NULL THEN 'sold'
    ELSE 'ended'
  END
  WHERE status = 'active'
  AND ends_at <= NOW();
  $$
);

Extension anti-snipe

La plupart des plateformes d'enchères prolongent la date limite si une enchère se déroule au cours des dernières secondes :

-- Ajouter au déclencheur process_new_bid
IF v_auction.ends_at - NOW() < INTERVAL '30 seconds' THEN
  UPDATE auctions
  SET ends_at = ends_at + INTERVAL '30 seconds'
  WHERE id = NEW.auction_id;
END IF;

Supabase Realtime vs alternatives

J'ai utilisé la plupart de ceux-ci en production. Voici une comparaison honnête :

Caractéristique Supabase Realtime Pusher Ably Firebase RTDB Socket.io (auto-hébergé)
Synchronisation de base de données native ✅ PostgreSQL WAL ❌ Service distinct ❌ Service distinct ✅ Arbre JSON ❌ Manuel
Latence (p99) ~80-100ms ~60ms ~50ms ~100ms ~40ms (dépend de l'infra)
Événements max/sec 200k+ 10k (Pro) 50k 100k Illimité (tu le mets à l'échelle)
Intégration d'authentification Intégrée (RLS + JWT) Personnalisée Basée sur des jetons Firebase Auth Personnalisée
Présence ✅ Intégrée ✅ Intégrée ✅ Intégrée ✅ Intégrée ✅ Intégrée
Niveau gratuit 500K MAU, 200 simultanés 100 connexions 6M msgs/mois 1GB stocké $0 (coûts d'hébergement)
Prix Pro $25/mois $49/mois $29/mois Paiement à l'utilisation ~$100-500/mois (AWS)
Meilleur pour Applications en temps réel centrées sur DB Pub/sub simple Haute fiabilité Applications mobiles Contrôle total

Pour un système d'enchères spécifiquement, Supabase gagne parce que tes enchères sont déjà dans PostgreSQL. Tu n'as pas besoin de synchroniser entre une base de données et un système pub/sub distinct. L'enchère frappe la DB, la DB déclenche le push WebSocket. Une seule source de vérité.

Si tu construis sur une architecture de CMS sans tête, Supabase s'adapte naturellement à côté de la distribution de contenu sans ajouter un autre service à gérer.

Déploiement et mise à l'échelle de ton système d'enchères

Pour la plupart des projets, le niveau Pro géré de Supabase à $25/mois gère confortablement jusqu'à 10 000 enchères quotidiennes. Voici ce à quoi il faut faire attention :

  • Limites de connexion : Le niveau Pro te donne 500 connexions Realtime simultanées. Si tu en as besoin de plus, tu devras mettre à niveau ou mettre en œuvre un pool de connexions côté client.
  • Taille WAL : Les enchères à haut volume génèrent un trafic WAL important. Surveille ton créneau de réplication pour éviter l'encrassement des disques.
  • Nombre de canaux : Chaque enchère obtient son propre canal. Avec des milliers d'enchères actives, teste que ton client se désabonne correctement des enchères terminées.

Pour un frontend construit avec Astro ou Next.js, le client Supabase JS fonctionne de manière identique — assure-toi simplement que tu l'initialises côté client pour les abonnements Realtime.

Si tu construis quelque chose qui doit gérer une mise à l'échelle sérieuse — des centaines de milliers d'enchérisseurs simultanés — contacte-nous. Nous avons conçu ces systèmes à l'échelle et nous pouvons t'aider à éviter les pièges. Tu peux également consulter notre page de tarification pour les engagements basés sur des projets.

FAQ

Combien d'enchérisseurs simultanés Supabase Realtime peut-il gérer ?

Supabase Realtime peut gérer plus de 200 000 événements par seconde sur des serveurs distribués sur leur plateforme gérée. Le niveau Pro à $25/mois supporte jusqu'à 500 connexions simultanées par projet. Pour les enchères plus grandes, le niveau Enterprise offre des limites personnalisées, ou tu peux auto-héberger le serveur Realtime (c'est open source) sur ta propre infrastructure.

Supabase Realtime est-il assez rapide pour une enchère en direct ?

Oui. Dans mes tests, la latence de bout en bout de l'insertion d'enchère à la notification du client s'élève en moyenne à environ 50-80 ms, avec p99 inférieur à 100 ms. Pour le contexte, un temps de réaction humain est d'environ 200-300 ms, donc les enchères apparaissent effectivement instantanées. Le goulot d'étranglement est rarement Supabase — c'est habituellement la connexion réseau du client.

Comment j'empêche les conditions de course quand deux personnes enchérissent simultanément ?

Utilise le verrouillage au niveau des lignes SELECT ... FOR UPDATE de PostgreSQL à l'intérieur d'une fonction de déclencheur, ou utilise des verrous consultatifs via pg_advisory_xact_lock(). Cela sérialise le traitement des enchères par enchère afin qu'une seule enchère soit validée à la fois. L'enchère « perdante » est toujours validée — elle voit simplement l'enchère la plus élevée mise à jour du gagnant et soit réussit (si elle est toujours plus élevée) soit échoue avec une erreur appropriée.

Puis-je utiliser Supabase Realtime avec Next.js ou Astro ?

Absolument. Le client @supabase/supabase-js fonctionne dans n'importe quel environnement JavaScript. Pour Next.js, initialise le client Supabase dans un composant client (puisque Realtime a besoin de WebSockets de navigateur) et utilise-le à l'intérieur de useEffect hooks. Pour Astro, utilise-le dans les îles interactives côté client. Le code d'abonnement est identique quel que soit ton choix de framework.

Que se passe-t-il si la connexion d'un utilisateur se coupe en plein milieu de l'enchère ?

Supabase Realtime tente automatiquement une reconnexion. Quand le client se reconnecte et se réabonne, il reçoit l'état actuel. Pour les enchères critiques, je recommande également de récupérer l'état de l'enchère le plus récent via une requête standard à la reconnexion pour assurer que rien n'a été manqué pendant la fenêtre de déconnexion. Le système Presence supprimera automatiquement l'utilisateur déconnecté après un délai.

Comment j'gère les heures de fin d'enchère avec précision ?

Ne fais jamais confiance aux minuteurs côté client pour les heures de fin d'enchère — ils peuvent être manipulés. Utilise l'extension PostgreSQL pg_cron pour vérifier et fermer les enchères expirées toutes les 10 secondes côté serveur. Envoie le timestamp du serveur aux clients afin qu'ils puissent afficher un compte à rebours, mais la détermination de la fin réelle se produit toujours dans la base de données.

Supabase Realtime est-il gratuit pour les petits projets ?

Le niveau gratuit de Supabase inclut Realtime avec jusqu'à 200 connexions simultanées et 500 000 utilisateurs actifs mensuels. C'est suffisant pour un site d'enchères d'hobby ou une MVP. Si tu gères une plateforme d'enchères de production avec du trafic significatif, le niveau Pro à $25/mois avec $0.09/GB de sortie est l'endroit où tu voudras commencer. C'est considérablement moins cher que de gérer ta propre infrastructure WebSocket.

Comment je teste un système d'enchères en temps réel localement ?

Utilise la CLI Supabase (supabase start) pour exécuter une instance Supabase locale avec Realtime activé. Ouvre plusieurs onglets de navigateur pour simuler plusieurs enchérisseurs. Pour les tests de charge, j'utilise un simple script Node.js qui crée 100+ clients Supabase et les fait enchérir l'un contre l'autre sur une minuterie. Cela détecte les conditions de course et t'aide à régler ton paramètre eventsPerSecond avant d'aller en production.