afterbuild/ops
ERR-122/Supabase · Realtime
ERR-122
Realtime channel reports SUBSCRIBED but no postgres_changes events fire for inserts or updates

appears when:After adding a .channel().on('postgres_changes', ...) handler that connects successfully but never receives a row

Supabase Realtime not working

Realtime sits on top of logical replication and RLS. If replication is off, or your SELECT policy blocks the row, or the JWT expired, the subscription is silent.

Last updated 17 April 2026 · 7 min read · By Hyder Shah
Direct answer
Supabase Realtime not working means one of four things: the table is not in the supabase_realtime publication, the authenticated user has no SELECT policy covering the changed row, the JWT on the channel has expired, or a React cleanup is missing so stale handlers drop events. Fix all four in order.

Quick fix for Supabase Realtime not working

app/hooks/useTasksRealtime.ts
typescript
01// app/hooks/useTasksRealtime.ts — typed subscription with cleanup and auth refresh02import { useEffect, useState } from "react";03import { createClient } from "@supabase/supabase-js";04 05const supabase = createClient(06  process.env.NEXT_PUBLIC_SUPABASE_URL!,07  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,08);09 10export function useTasksRealtime(userId: string) {11  const [tasks, setTasks] = useState<Task[]>([]);12 13  useEffect(() => {14    const channel = supabase15      .channel(`tasks:${userId}`)16      .on(17        "postgres_changes",18        { event: "*", schema: "public", table: "tasks", filter: `user_id=eq.${userId}` },19        (payload) => {20          if (payload.eventType === "INSERT") setTasks((t) => [...t, payload.new as Task]);21          if (payload.eventType === "DELETE") setTasks((t) => t.filter((x) => x.id !== payload.old.id));22        },23      )24      .subscribe();25 26    // refresh the JWT on every auth event so RLS stays accurate27    const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => {28      if (session?.access_token) supabase.realtime.setAuth(session.access_token);29    });30 31    return () => {32      supabase.removeChannel(channel);33      sub.subscription.unsubscribe();34    };35  }, [userId]);36 37  return tasks;38}39 40type Task = { id: string; user_id: string; title: string };
Typed hook — handles INSERT, DELETE, token refresh, and channel cleanup in one place

Deeper fixes when the quick fix fails

01 · Enable replication via SQL migration (reproducible)

supabase/migrations/20260417_enable_tasks_realtime.sql
sql
01-- supabase/migrations/20260417_enable_tasks_realtime.sql02-- Add the table to the built-in supabase_realtime publication03alter publication supabase_realtime add table public.tasks;04 05-- If you also stream UPDATEs and DELETEs, ensure the REPLICA IDENTITY06-- is FULL so the payload includes old values (Supabase default is DEFAULT)07alter table public.tasks replica identity full;08 09-- Matching SELECT policy so the user can read events about their own rows10create policy "users read own tasks"11on public.tasks12for select13to authenticated14using (auth.uid() = user_id);
Check this into source control so Realtime config is reproducible across environments

02 · Typed subscription wrapper that handles lifecycle

lib/realtime.ts
typescript
01// lib/realtime.ts — generic subscribe helper with cleanup built in02import type { RealtimeChannel, SupabaseClient } from "@supabase/supabase-js";03 04type ChangePayload<T> =05  | { eventType: "INSERT"; new: T; old: null }06  | { eventType: "UPDATE"; new: T; old: T }07  | { eventType: "DELETE"; new: null; old: T };08 09export function subscribeToTable<T>(10  supabase: SupabaseClient,11  table: string,12  filter: string,13  onChange: (p: ChangePayload<T>) => void,14): () => void {15  const channel: RealtimeChannel = supabase16    .channel(`${table}:${filter}`)17    .on("postgres_changes", { event: "*", schema: "public", table, filter }, (raw) => {18      onChange(raw as unknown as ChangePayload<T>);19    })20    .subscribe();21 22  return () => {23    supabase.removeChannel(channel);24  };25}
Centralizes subscribe + cleanup so no component forgets removeChannel

03 · Debug the drop reason in Realtime logs

Dashboard → Logs → Realtime. Filter by your channel id. Every dropped event is logged with a reason — rls_violation, no_policy, or publication_missing. This is faster than guessing. If logs show rls_violation but your policy looks correct, confirm the JWT on the channel with supabase.realtime.accessToken and ensure it matches the expected user.

Why AI-built apps hit Supabase Realtime not working

Supabase Realtime is a layer on top of Postgres logical replication and the built-in Realtime server. When a row changes, Postgres writes a WAL entry, the Realtime service reads the WAL, evaluates RLS against every subscribed connection, and forwards the event only to clients that should see the row. Three separate things must be true for the event to arrive in your browser: replication enabled on the table, RLS permitting SELECT for the subscriber, and the channel carrying a valid user JWT.

AI builders wire up the client SDK with supabase.channel().on(...) and stop there. The table is not added to the supabase_realtime publication by default. The channel reports SUBSCRIBED because the websocket handshake succeeded, but Postgres never streams the changes into the Realtime service. Nothing ever arrives. The fix is two lines of SQL or one toggle in the Supabase Dashboard under Database → Replication.

The second common failure is RLS. Realtime evaluates your SELECT policy against each subscriber. If the subscriber is anonymous and your policy requires auth.uid() = user_id, the row is filtered and the event is dropped silently. AI-generated code commonly sets up Realtime with the anon key before the user signs in, or forgets to call supabase.realtime.setAuth after the user authenticates. The channel stays alive on the stale identity and events never reach the handler.

The third failure is React cleanup. useEffect must return a function that calls supabase.removeChannel(channel). Without it, every route change or strict-mode remount leaks a new channel. Two channels get every event, one of which updates the stale state from the unmounted component. The user sees duplicate rows or ghost updates.

Supabase Realtime not working by AI builder

How often each AI builder ships this error and the pattern that produces it.

AI builder × Supabase Realtime not working
BuilderFrequencyPattern
LovableEvery Realtime scaffoldAdds subscribe() but never enables replication on the table
Bolt.newCommonSubscribes with anon key before user signs in; never calls setAuth
Base44CommonNo SELECT policy — RLS silently drops every event
CursorSometimesuseEffect with no cleanup leaks duplicate channels in strict mode
Replit AgentRareUses REPLICA IDENTITY DEFAULT so UPDATE payloads are missing old values

Related errors we fix

Stop Supabase Realtime not working recurring in AI-built apps

Still stuck with Supabase Realtime not working?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

Supabase Realtime not working questions

Why does my Supabase Realtime subscription connect but never fire?+
Realtime runs on top of Postgres logical replication. If the table is not added to the supabase_realtime publication, Postgres never forwards changes to the Realtime service and subscribers sit idle. The channel reports SUBSCRIBED because the websocket handshake succeeded, but no rows ever stream. Fix by toggling Realtime on in the Dashboard or running the equivalent SQL to add the table to the publication.
Does Row Level Security affect Realtime?+
Yes and this catches almost every AI-built app. Realtime enforces RLS on every row before it sends the event. If your SELECT policy blocks the row for the authenticated user, the event is silently dropped. The client sees nothing. Fix by writing a SELECT policy that matches the ownership column and by confirming the realtime subscription uses a user-scoped access token, not the anon key.
What does Realtime do when my Supabase access token expires?+
The token is set at channel creation. When it expires Supabase keeps the websocket open but stops matching RLS against the expired JWT, so events you used to receive start getting dropped. Call supabase.realtime.setAuth(newToken) inside your onAuthStateChange handler so the channel refreshes its identity. AI builders rarely wire this up; the bug only surfaces after an hour of idle usage.
Why do I see duplicate Realtime events in React?+
React 18 strict mode mounts and unmounts every effect twice in development, which creates two channel subscriptions. Both stay alive because the cleanup function does not actually remove the channel. Always return () => supabase.removeChannel(channel) from useEffect. In production the double-mount does not happen but a route change without cleanup leaks the old channel into the next page.
How long does a Supabase Realtime fix take?+
Twenty minutes for the common case: enable replication on the table, write the SELECT policy, and add a cleanup function. Longer if you need to refactor the subscription into a typed helper or add setAuth for token refresh. Security Audit at $499 includes a full Realtime audit, pgTAP tests, and a regression suite so policies cannot drift.
Next step

Ship the fix. Keep the fix.

Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources