Corrige le Realtime qui ne diffusait aucun événement à travers l'app
Build and deploy / deploy (push) Successful in 37s

Les canaux Realtime (Char, Gardien, classement, journal, calendrier,
Icare) s'abonnaient juste après createClient(), avant que la session
ne soit chargée de façon asynchrone — le canal rejoignait donc
Supabase en rôle "anon" au lieu de "authenticated", et les policies
RLS "to authenticated" bloquaient silencieusement tous les événements
(l'abonnement lui-même réussissait, ce qui masquait le problème).

Ajoute waitForRealtimeAuth() dans src/lib/supabase/client.ts, qui
attend la session et pousse explicitement le token avant de
s'abonner. Vérifié avec deux sessions navigateur distinctes : les
changements se propagent maintenant réellement sans rechargement.
This commit is contained in:
Valentin ROBIN
2026-08-19 00:40:02 +02:00
parent 1e7ff621e4
commit 0c4814f4d6
7 changed files with 128 additions and 70 deletions
+10 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
import { DayCard, type DayRow } from "./day-card";
import { PANTHEON_PRESETS, getGodPreset } from "@/lib/pantheon";
import { IconPlus } from "@/components/icons";
@@ -69,15 +69,22 @@ export function CalendrierView({
useEffect(() => {
const supabase = createClient();
const channel = supabase
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("calendrier-changes")
.on("postgres_changes", { event: "*", schema: "public", table: "days" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "events" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
.subscribe();
});
return () => {
supabase.removeChannel(channel);
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, []);
+10 -3
View File
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
import { EntryRanking } from "./entry-ranking";
import { GardienBar } from "./gardien-bar";
import { RouletteSection } from "./roulette-section";
@@ -64,16 +64,23 @@ export function CharView({
useEffect(() => {
const supabase = createClient();
const channel = supabase
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("char-changes")
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_slots" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_entries" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_questions" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
.subscribe();
});
return () => {
supabase.removeChannel(channel);
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, [refetch]);
+10 -3
View File
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useState } from "react";
import { IconChevronDown, IconPencil, IconPlus, IconTrash } from "@/components/icons";
import { createClient } from "@/lib/supabase/client";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
import type { QuestionRow } from "./page";
function QuestionForm({
@@ -211,14 +211,21 @@ export function QuestionsView({
useEffect(() => {
const supabase = createClient();
const channel = supabase
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("char-questions-changes")
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_questions" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
.subscribe();
});
return () => {
supabase.removeChannel(channel);
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, [refetch]);
+10 -3
View File
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Avatar } from "@/components/avatar";
import { computeRanksBy } from "@/lib/ranking";
import { createClient } from "@/lib/supabase/client";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
import { IcarusGame } from "./icarus-game";
import type { IcarusScoreEntry } from "./page";
@@ -45,15 +45,22 @@ export function IcareView({
useEffect(() => {
const supabase = createClient();
const channel = supabase
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("icarus-scores")
.on("postgres_changes", { event: "*", schema: "public", table: "icarus_scores" }, () => {
refetch();
})
.subscribe();
});
return () => {
supabase.removeChannel(channel);
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, [refetch]);
+10 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
type Entry = {
id: number;
@@ -31,7 +31,12 @@ export function JournalView({
useEffect(() => {
const supabase = createClient();
const channel = supabase
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("journal-points-log")
.on(
"postgres_changes",
@@ -42,9 +47,11 @@ export function JournalView({
},
)
.subscribe();
});
return () => {
supabase.removeChannel(channel);
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, []);
+10 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
import { Avatar } from "@/components/avatar";
import { Podium } from "@/components/podium";
import { JudgePointControls } from "@/components/judge-point-controls";
@@ -47,7 +47,12 @@ export function LeaderboardView({
useEffect(() => {
const supabase = createClient();
const channel = supabase
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("leaderboard-profiles")
.on(
"postgres_changes",
@@ -72,9 +77,11 @@ export function LeaderboardView({
},
)
.subscribe();
});
return () => {
supabase.removeChannel(channel);
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, []);
+16
View File
@@ -1,4 +1,5 @@
import { createBrowserClient } from "@supabase/ssr";
import type { SupabaseClient } from "@supabase/supabase-js";
export function createClient() {
return createBrowserClient(
@@ -6,3 +7,18 @@ export function createClient() {
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
// createClient() charge la session (et donc le token Realtime) de façon
// asynchrone après coup : un .channel().subscribe() lancé juste après
// rejoint le canal encore en rôle "anon", et les policies RLS
// "to authenticated" ne laissent alors jamais passer aucun événement
// (le canal s'abonne "avec succès" mais ne reçoit plus rien ensuite).
// À appeler avant tout .channel(...).subscribe().
export async function waitForRealtimeAuth(supabase: SupabaseClient) {
const {
data: { session },
} = await supabase.auth.getSession();
if (session) {
supabase.realtime.setAuth(session.access_token);
}
}