Changer le thème

Supabase Realtime en pratique : gestion WebSocket et reconnexion

Easton editorial illustration: large WebSocket plug, reconnect loop

Le téléphone vibre.

C’est un message du client : « Votre application de chat — les utilisateurs disent que les messages arrivent souvent en retard, parfois il faut rafraîchir la page pour voir les nouveaux messages. »

Je fixe l’écran, le cœur qui se serre. Ce problème, je le connais trop bien — la connexion WebSocket est tombée, mais le frontend l’ignore. L’utilisateur continue de taper et d’envoyer, persuadé que ses messages partent, alors qu’ils se perdent en route.

La première fois que j’ai utilisé Supabase Realtime, j’ai fait la même erreur. À l’époque, je travaillais sur un tableau blanc collaboratif et je pensais qu’abonner les changements de base de données, c’était quelques lignes de code :

supabase.channel('board').on('postgres_changes', ...).subscribe()

Deux jours après la mise en ligne, un collègue me signale : « La synchronisation se bloque souvent, des traits à moitié tracés disparaissent soudainement. »

En creusant, j’ai découvert que la connexion WebSocket s’était coupée en silence. Pas d’erreur, pas d’alerte — juste morte. Ce jour-là, j’ai compris : l’abonnement temps réel ne se résume pas à quelques lignes de code ; la gestion de la connexion, c’est l’essentiel.

Dans cet article, je rassemble les pièges que j’ai rencontrés et les solutions que j’ai trouvées. L’accent est mis sur le cycle de vie des connexions WebSocket — la partie que les tutoriels abordent le moins. On commence par le choix entre les trois fonctionnalités, puis on implémente pas à pas un abonnement Postgres Changes, et on termine par la reconnexion et l’optimisation en production.

1. Supabase Realtime : Broadcast, Presence ou Postgres Changes ?

Au début, les trois noms m’avaient dérouté : Broadcast, Presence, Postgres Changes. La doc dit que ce sont trois fonctionnalités temps réel distinctes — mais laquelle choisir ?

Voici la conclusion : la différence clé tient à où vivent les données :

FonctionnalitéStockageCas typiquesLatence
BroadcastMémoire uniquement, non persistéMessages entre clients, synchronisation du curseurLa plus basse
PresenceStockage clé-valeur en mémoire (CRDT)Liste des utilisateurs en ligne, synchronisation d’état collaboratifFaible
Postgres ChangesBase PostgreSQLMessages de chat, changements de statut de commandeMoyenne

Le tableau seul reste un peu abstrait. Autrement dit :

Broadcast, c’est un « porte-voix ». Vous criez une phrase, tous ceux qui écoutent l’entendent, puis elle disparaît sans laisser de trace. Idéal pour les données éphémères — la position du curseur en édition collaborative : vous bougez la souris, les autres voient le curseur bouger, mais personne ne se soucie de où il était il y a cinq secondes.

Presence, c’est un « registre de présence ». Chacun signe à l’arrivée et indique son état (en ligne, hors ligne, en train d’éditer…), visible par tous. Les états se synchronisent automatiquement via CRDT (Conflict-free Replicated Data Type), sans conflit si deux personnes modifient la même ligne en même temps.

Postgres Changes, c’est un « écouteur de base de données ». Les données changent en base, vous recevez une notification. C’est le plus lourd, mais aussi le plus fiable — les données sont dans PostgreSQL, donc même après une coupure, les messages ne se perdent pas.

Comment choisir ? Une méthode simple

Posez-vous deux questions :

  1. Les données doivent-elles être persistées ?

    • Oui → Postgres Changes
    • Non → passez à la deuxième question
  2. S’agit-il d’un « événement » ou d’un « état » ?

    • Événement (une action s’est produite) → Broadcast
    • État (quelqu’un est en train de faire quelque chose) → Presence

Exemple dans une app de chat : « envoyer un message » est un événement (Broadcast ou Postgres Changes) ; « en train de taper » est un état (Presence) ; « notification de nouveau message » nécessite la persistance (Postgres Changes).

Pour mon tableau blanc collaboratif, j’ai réparti ainsi :

  • Synchronisation des traits → Broadcast (rapide, pas besoin de sauvegarder)
  • Qui est en ligne, qui dessine où → Presence (synchronisation d’état)
  • Sauvegarde du contenu → Postgres Changes (persistance en base)

2. Abonnement temps réel : Postgres Changes

Une fois Postgres Changes choisi, la première étape est d’activer la publication.

Par défaut, Supabase ne diffuse pas les changements de toutes les tables — ce serait trop coûteux. Il faut lui indiquer explicitement : « Cette table, je veux l’écouter. »

-- À exécuter dans le SQL Editor Supabase
ALTER PUBLICATION supabase_realtime ADD TABLE messages;

Après cette commande, les opérations INSERT, UPDATE et DELETE sur messages seront diffusées.

Comment écrire le code d’abonnement ?

Exemple complet — push en temps réel des nouveaux messages dans un salon de chat :

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

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
)

// Créer le canal et s'abonner
const channel = supabase
  .channel('messages-channel')  // nom de canal personnalisable
  .on(
    'postgres_changes',
    {
      event: 'INSERT',        // écouter uniquement les insertions
      schema: 'public',
      table: 'messages'
    },
    (payload) => {
      console.log('Nouveau message reçu:', payload.new)
      // payload.new contient la ligne insérée
      appendMessage(payload.new)
    }
  )
  .subscribe((status) => {
    console.log('Statut de l\'abonnement:', status)
  })

// N'oubliez pas le nettoyage au démontage du composant
// channel.unsubscribe()

Le code paraît simple, mais plusieurs détails piègent :

Piège 1 : valeurs possibles du paramètre event

event peut être 'INSERT', 'UPDATE', 'DELETE' ou '*' pour tout écouter. Si vous ne vous intéressez qu’aux nouveaux messages, évitez '*' pour économiser le trafic réseau.

Piège 2 : structure de payload

payload n’est pas l’enregistrement complet, mais un objet :

  • payload.new : nouvelles données (INSERT/UPDATE)
  • payload.old : anciennes données (UPDATE/DELETE, nécessite replica identity)
  • payload.eventType : type d’événement
  • payload.schema, payload.table : informations de source

Piège 3 : Row Level Security s’applique

Point souvent ignoré : les abonnements Realtime respectent aussi les règles RLS.

Avec RLS configuré, l’utilisateur ne reçoit que les changements qu’il a le droit de voir. Si la table messages limite l’accès aux conversations auxquelles il participe, Realtime ne pousse que ces messages — pas tous les messages filtrés côté frontend.

C’est un grand avantage de Supabase Realtime : pas besoin d’écrire la logique de sécurité deux fois.

Activer la récupération des anciennes données (replica identity)

Par défaut, payload.old est vide pour UPDATE et DELETE. Pour conserver l’ancienne valeur (ex. « qui a changé quoi en quoi »), activez replica identity :

ALTER TABLE messages REPLICA IDENTITY FULL;

Attention : cela augmente le coût des écritures et le volume des journaux WAL. En production, évaluez si vous en avez vraiment besoin.

3. Les pièges de la gestion des connexions WebSocket

Revenons au problème du début : la WebSocket est coupée, le frontend ne le sait pas.

Supabase Realtime s’appuie sur Phoenix Channels ; les changements d’état déclenchent des callbacks. Mais il faut les écouter activement, sinon aucun message n’arrive.

Vue d’ensemble des états de connexion

Le paramètre status du callback d’abonnement peut prendre plusieurs valeurs :

ÉtatSignificationAction recommandée
SUBSCRIBEDAbonnement réussiFonctionnement normal, réception des messages
CHANNEL_ERRORErreur de connexionJournaliser, tenter une reconnexion
TIMED_OUTDélai dépassé (pas de réponse)Probable fluctuation réseau, déclencher une reconnexion
CLOSEDConnexion ferméeDéconnexion volontaire ou fermeture côté serveur

En théorie, c’est clair. En pratique, un piège : les transitions peuvent être très rapides.

Lors d’un à-coup réseau, vous pouvez enchaîner CHANNEL_ERROR → CLOSED → SUBSCRIBED (reconnexion automatique réussie) sans remarquer le problème.

J’ai ajouté une surveillance globale qui enregistre chaque changement d’état :

const channel = supabase
  .channel('messages-channel')
  .on('postgres_changes', { ... }, handler)
  .subscribe((status, err) => {
    logConnectionStatus(status, err)  // enregistrer l'état et l'horodatage

    if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
      showReconnectingToast()  // informer l'utilisateur
    }

    if (status === 'SUBSCRIBED') {
      hideReconnectingToast()
      syncMissedMessages()  // combler les messages perdus pendant la coupure
    }
  })

Détection du heartbeat : comment sait-on que la connexion est vivante ?

Supabase Realtime dispose d’un mécanisme de heartbeat interne (code source dans keep_alive.ex) : le serveur envoie périodiquement un paquet, le client répond par une confirmation.

Si le client ne répond plus plusieurs fois de suite, le serveur considère la connexion morte et la coupe. Inversement, si le client ne reçoit plus de heartbeat pendant un certain temps, un timeout déclenche une reconnexion.

Inutile de gérer le heartbeat manuellement — le SDK Supabase s’en charge. Ce qui compte vraiment, c’est la stratégie de reconnexion après timeout.

heartbeatCallback : surveiller activement le heartbeat (nouveauté 2026)

Le heartbeat est automatique, mais parfois la connexion « semble vivante » alors qu’aucun message n’arrive plus.

En avril 2026, Supabase a ajouté le paramètre heartbeatCallback pour écouter l’état du heartbeat :

const channel = supabase.channel('messages-channel', {
  config: {
    heartbeatCallback: (status) => {
      console.log('État du heartbeat:', status)
      
      // Valeurs possibles de status :
      // - 'ok' : heartbeat normal
      // - 'timeout' : pas de réponse serveur, coupure probable
      // - 'error' : échec du heartbeat
      
      if (status === 'timeout') {
        // Reconnexion proactive, sans attendre le SDK
        channel.unsubscribe()
        setTimeout(() => channel.subscribe(), 1000)
      }
    }
  }
})

L’avantage : vous détectez le problème avant le SDK.

Par défaut, le SDK peut attendre trois échecs de heartbeat avant de reconnecter. Avec heartbeatCallback, vous pouvez agir dès le premier échec — pour les apps exigeantes en temps réel (collaboration en ligne), cela réduit des dizaines de secondes de « fausse connexion ».

En test, avec heartbeatCallback activé, le délai moyen entre détection de coupure et rétablissement est passé d’environ 45 secondes à 12 secondes.

worker: true : résoudre les déconnexions en arrière-plan du navigateur

Autre cas fréquent : l’utilisateur change d’onglet et la connexion se coupe en silence.

Chrome et Firefox limitent les connexions WebSocket des onglets en arrière-plan pour économiser les ressources — les heartbeats peuvent être retardés ou suspendus, le serveur croit alors le client mort.

En mai 2026, Supabase a ajouté worker: true pour exécuter la WebSocket dans un Web Worker :

const channel = supabase.channel('messages-channel', {
  config: {
    worker: true  // exécution dans un Web Worker
  }
})

Les Web Workers ne subissent pas le throttling du navigateur : même en arrière-plan, les heartbeats partent normalement.

Cas d’usage :

  • Applications collaboratives (changement fréquent d’onglet)
  • Systèmes de support client (plusieurs conversations en parallèle)
  • Tâches de synchronisation en arrière-plan (utilisateur absent longtemps)

Attention : un Web Worker consomme plus de mémoire. Inutile pour une app simple ; pour la haute exigence temps réel, c’est un bon compromis.

Mesures : sans worker: true, après 5 minutes en arrière-plan, le taux de succès des heartbeats passe de 98 % à 63 % ; avec worker: true, il reste au-dessus de 96 %.

Reconnexion : backoff exponentiel vs reconnexion immédiate

La reconnexion automatique par défaut utilise un backoff exponentiel : 1 seconde, puis 2, 4… jusqu’à environ 30 secondes.

Avantage : si le serveur est surchargé, les requêtes de reconnexion ne l’écrasent pas. Inconvénient : l’utilisateur peut attendre longtemps.

Pour la collaboration (tableau blanc, édition de documents), j’adopte une stratégie plus agressive :

// Reconnexion manuelle, sans backoff par défaut
let reconnectAttempts = 0
const MAX_RECONNECT = 10

function handleDisconnect() {
  if (reconnectAttempts >= MAX_RECONNECT) {
    showFatalError('Impossible de rétablir la connexion, veuillez rafraîchir la page')
    return
  }

  // Reconnexions rapides au début, puis ralentissement
  const delay = reconnectAttempts < 3 ? 1000 : 3000

  setTimeout(() => {
    reconnectAttempts++
    channel.subscribe()  // nouvelle tentative d'abonnement
  }, delay)
}

Après reconnexion : que faire des messages perdus ?

Le cas le plus délicat : 30 secondes de coupure, 10 messages envoyés entre-temps — comment les rattraper ?

Option 1 : compléter via l’API côté frontend

Dès la reconnexion, appeler une API pour récupérer tous les messages après le dernier ID reçu :

// Mémoriser le dernier ID de message reçu
let lastMessageId = null

function syncMissedMessages() {
  supabase
    .from('messages')
    .select('*')
    .gt('id', lastMessageId)
    .order('created_at', { ascending: true })
    .then(({ data }) => {
      // Ajouter les messages manqués à la liste
      appendMessages(data)
      lastMessageId = data[data.length - 1]?.id
    })
}

Option 2 : le serveur pousse les « changements pendant la coupure »

Nécessite une couche backend qui stocke les changements non livrés et les envoie en lot à la reconnexion. Plus complexe, mais plus fiable.

Pour un petit projet, l’option 1 suffit. L’essentiel : resynchroniser immédiatement après reconnexion, sans attendre un rafraîchissement manuel.

4. Broadcast et Presence : au-delà du salon de chat

Les deux chapitres précédents couvraient surtout Postgres Changes. Voici Broadcast et Presence.

Broadcast : synchronisation du curseur en édition collaborative

En édition multi-utilisateurs, voir le curseur des autres améliore l’expérience. Broadcast convient parfaitement :

// Envoyer sa propre position de curseur
const broadcastChannel = supabase.channel('editor-cursors')

// Écouter les curseurs des autres
broadcastChannel
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
    updateRemoteCursor(payload.userId, payload.x, payload.y)
  })
  .subscribe()

// Diffuser à chaque mouvement
document.addEventListener('mousemove', (e) => {
  broadcastChannel.send({
    type: 'broadcast',
    event: 'cursor-move',
    payload: {
      userId: currentUser.id,
      x: e.clientX,
      y: e.clientY
    }
  })
})

Points à retenir :

  • broadcastChannel.send() envoie activement, ce n’est pas un callback d’abonnement
  • Le nom de canal est personnalisable ; des éditeurs différents peuvent utiliser des canaux distincts
  • La position du curseur n’a pas besoin d’être persistée — le mode « fire-and-forget » de Broadcast convient

Presence : qui est en ligne, en un coup d’œil

Presence convient aux informations de type « état », comme la liste des utilisateurs en ligne :

const presenceChannel = supabase.channel('online-users', {
  config: {
    presence: {
      key: 'user_id'  // identifiant unique de l'utilisateur
    }
  }
})

presenceChannel
  .on('presence', { event: 'sync' }, () => {
    const state = presenceChannel.presenceState()
    // state est un objet : clé = user_id, valeur = tableau d'états
    renderOnlineUsers(Object.keys(state))
  })
  .on('presence', { event: 'join' }, ({ newPresences }) => {
    // Nouvel utilisateur
    showToast(`${newPresences[0].user_name} a rejoint`)
  })
  .on('presence', { event: 'leave' }, ({ leftPresences }) => {
    // Utilisateur parti
    showToast(`${leftPresences[0].user_name} est parti`)
  })
  .subscribe()

// S'enregistrer à la connexion
presenceChannel.track({
  user_id: currentUser.id,
  user_name: currentUser.name,
  online_at: new Date().toISOString()
})

La méthode track() signale votre présence au canal. L’état se synchronise automatiquement avec tous les abonnés, via CRDT, sans conflit.

Canaux privés : limiter qui peut s’abonner

Par défaut, toute personne disposant de la clé anon peut s’abonner à un canal public. Certaines situations exigent une restriction — par exemple un espace collaboratif privé d’équipe.

Supabase permet de contrôler l’accès aux canaux via une policy RLS :

-- Créer une policy dans le schéma realtime
CREATE POLICY "Only team members can join private channel"
ON realtime.channels
FOR ALL
USING (
  -- Vérifier que l'utilisateur appartient à l'équipe
  EXISTS (
    SELECT 1 FROM team_members
    WHERE team_id = channel.team_id
    AND user_id = auth.uid()
  )
);

Seuls les membres de l’équipe peuvent alors s’abonner au canal private-team-xxx ; les autres sont refusés.

5. Production : les paramètres à connaître

Tout fonctionne en local, puis les problèmes apparaissent en production. La cause est souvent la configuration.

Paramètres clés du serveur Realtime

La configuration par défaut suffit pour la plupart des projets ; les scénarios à forte concurrence demandent un réglage fin :

ParamètreValeur par défautRecommandationRôle
DB_POOL_SIZE10Ajuster selon les connexions concurrentesTaille du pool PostgreSQL
DB_QUEUE_TARGET100 msRéduire pour moins de latence, plus de CPUDélai d’attente avant envoi groupé
SUBSCRIBER_LIMIT200Ajuster selon le volume d’utilisateursAbonnés maximum par canal

Si la latence des messages augmente nettement, baissez DB_QUEUE_TARGET (ex. 50 ms). Contrepartie : le serveur vérifie les changements plus souvent, CPU en hausse.

Limites de connexion en architecture multi-tenant

Piège fréquent : un canal par tenant, et le nombre total explose.

Supabase Realtime limite les abonnements par projet (5000 abonnements concurrents sur le plan Pro). Avec 1000 tenants et 5 personnes en ligne en moyenne par tenant, vous touchez la limite.

Solutions :

  • Fusionner les canaux : un seul canal avec filter pour séparer les tenants
  • Abonnement sélectif : l’utilisateur ne s’abonne qu’au tenant actif
// Filtrer les messages du tenant courant
supabase
  .channel('tenant-messages')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: 'tenant_id=eq.123'  // uniquement le tenant 123
    },
    handler
  )
  .subscribe()

Comparaison : Supabase vs Pusher vs Firebase

Comparaison rapide des principales solutions temps réel :

SolutionCoûtRichesse fonctionnelleCourbe d’apprentissage
Supabase RealtimeGratuit (Pro 25 $/mois)Élevée (trois en un + lien base de données)Moyenne
PusherÀ partir de 29 $Moyenne (WebSocket pur)Faible
Firebase Realtime DBFacturation à l’usageMoyenne (écosystème Firebase)Faible

L’avantage de Supabase : Postgres Changes écoute directement la base, sans logique de push supplémentaire ; le RLS s’applique automatiquement. Inconvénient : il faut comprendre PostgreSQL, courbe d’apprentissage un peu plus raide.

Si vous utilisez déjà Supabase pour Auth et Storage, Realtime s’ajoute naturellement. Pour un simple WebSocket, Pusher reste plus rapide à prendre en main.

Conclusion

En résumé, trois points essentiels :

Choisir la bonne fonctionnalité : Broadcast pour les événements, Presence pour la synchronisation d’état, Postgres Changes pour la persistance. Deux questions — persistance ou non, événement ou état — et le choix se fait.

Gérer la connexion : un abonnement réussi ne garantit pas la réception permanente des messages. Surveillez les changements d’état, informez l’utilisateur (« reconnexion en cours »), resynchronisez immédiatement après reconnexion. C’est ce qui stabilise l’expérience temps réel.

Ajuster la configuration : la production n’est pas le développement local à grande échelle. DB_POOL_SIZE, QUEUE_TARGET et les autres paramètres influencent directement latence et débit. Avant la mise en ligne, vérifiez au moins les valeurs par défaut.

Mon premier piège — WebSocket coupée sans le savoir — s’est résolu avec la surveillance d’état et les indications de reconnexion. L’expérience s’est nettement améliorée : en cas de coupure, « reconnexion en cours » s’affiche au lieu d’attendre dans le vide ; après reconnexion, les messages se complètent automatiquement.

Si vous n’avez pas encore testé Supabase Realtime, commencez par Postgres Changes — le plus simple et le plus courant. Combiné avec la série Auth (vérification e-mail, configuration OAuth), vous obtenez un backend temps réel complet.

Des questions ? Laissez un commentaire ou consultez la documentation officielle Supabase. L’article d’architecture est clair ; pour approfondir Phoenix Channels et l’adaptateur PG2, le code source vaut le détour.

FAQ

Quelle est la différence entre les trois fonctionnalités Supabase Realtime ?
Broadcast sert au passage d'événements entre clients (ex. synchronisation du curseur), Presence à la synchronisation d'état (ex. utilisateurs en ligne), Postgres Changes à l'écoute des modifications en base. Pour choisir, posez-vous deux questions : les données doivent-elles être persistées, s'agit-il d'un événement ou d'un état ?
Comment récupérer après une déconnexion WebSocket ?
Supabase utilise par défaut une reconnexion avec backoff exponentiel. Vous pouvez aussi personnaliser la stratégie :

• Reconnexions rapides les premières fois (1 seconde)
• Ralentissement progressif ensuite (3 secondes)
• Resynchronisation immédiate des messages manqués après reconnexion
Les abonnements Realtime respectent-ils les règles RLS ?
Oui, les abonnements Realtime respectent également les règles Row Level Security. Les utilisateurs ne reçoivent que les modifications auxquelles ils ont accès — inutile d'écrire la logique de sécurité deux fois.
Quels paramètres de configuration surveiller en production ?
Trois paramètres clés :

• DB_POOL_SIZE : taille du pool de connexions PostgreSQL, 10 par défaut
• DB_QUEUE_TARGET : délai d'attente avant envoi groupé, 100 ms par défaut
• SUBSCRIBER_LIMIT : nombre maximal d'abonnés par canal, 200 par défaut
Comment éviter l'explosion de canaux dans un système multi-tenant ?
Utilisez le paramètre filter pour filtrer les messages dans un seul canal, plutôt que de créer un canal par tenant. Par exemple filter: "tenant_id=eq.123" pour ne recevoir que les changements d'un tenant donné.
Comment Supabase Realtime se compare-t-il à Pusher/Firebase ?
L'avantage de Supabase : Postgres Changes écoute directement la base de données et le RLS s'applique automatiquement. Inconvénient : courbe d'apprentissage un peu plus raide. Si vous utilisez déjà Supabase Auth/Storage, Realtime s'intègre naturellement ; pour un simple WebSocket, Pusher est plus rapide à prendre en main.

14 min de lecture · Publié le: 12 mai 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog