Supabase Realtime en pratique : gestion WebSocket et reconnexion

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é | Stockage | Cas typiques | Latence |
|---|---|---|---|
| Broadcast | Mémoire uniquement, non persisté | Messages entre clients, synchronisation du curseur | La plus basse |
| Presence | Stockage clé-valeur en mémoire (CRDT) | Liste des utilisateurs en ligne, synchronisation d’état collaboratif | Faible |
| Postgres Changes | Base PostgreSQL | Messages de chat, changements de statut de commande | Moyenne |
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 :
-
Les données doivent-elles être persistées ?
- Oui → Postgres Changes
- Non → passez à la deuxième question
-
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énementpayload.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 :
| État | Signification | Action recommandée |
|---|---|---|
SUBSCRIBED | Abonnement réussi | Fonctionnement normal, réception des messages |
CHANNEL_ERROR | Erreur de connexion | Journaliser, tenter une reconnexion |
TIMED_OUT | Délai dépassé (pas de réponse) | Probable fluctuation réseau, déclencher une reconnexion |
CLOSED | Connexion fermée | Dé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ètre | Valeur par défaut | Recommandation | Rôle |
|---|---|---|---|
DB_POOL_SIZE | 10 | Ajuster selon les connexions concurrentes | Taille du pool PostgreSQL |
DB_QUEUE_TARGET | 100 ms | Réduire pour moins de latence, plus de CPU | Délai d’attente avant envoi groupé |
SUBSCRIBER_LIMIT | 200 | Ajuster selon le volume d’utilisateurs | Abonné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
filterpour 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 :
| Solution | Coût | Richesse fonctionnelle | Courbe d’apprentissage |
|---|---|---|---|
| Supabase Realtime | Gratuit (Pro 25 $/mois) | Élevée (trois en un + lien base de données) | Moyenne |
| Pusher | À partir de 29 $ | Moyenne (WebSocket pur) | Faible |
| Firebase Realtime DB | Facturation à l’usage | Moyenne (é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 ?
Comment récupérer après une déconnexion WebSocket ?
• 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 ?
Quels paramètres de configuration surveiller en production ?
• 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 ?
Comment Supabase Realtime se compare-t-il à Pusher/Firebase ?
14 min de lecture · Publié le: 12 mai 2026 · Mis à jour le: 27 juil. 2026
Supabase en pratique
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Supabase Realtime en pratique : trois modes comparés et apps collaboratives
Supabase Realtime propose trois modes : Postgres Changes, Presence et Broadcast. Comparaison des forces de chacun, exemples complets d'app collaborative et configuration RLS sécurisée.
Partie 5 sur 10
Suivant
Supabase Storage en pratique : upload, CDN et contrôle d'accès
Guide complet Supabase Storage : trois modes de contrôle d'accès comparés, upload TUS par morceaux, optimisation Smart CDN, analyse des prix vs R2/S3. Exemples React et dépannage.
Partie 7 sur 10



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire