Realtime postgres_changes not working in production

Hi

I'm using Nextjs 15 (app router), Supabase with Clerk integration and attempting to use realtime for my 'submissions' table.

Everything works as expected, when developing locally. In either case, I seem to be able to get the current token with no issues. Token then seems to get used as the apikey param in the wss:// url.

hook:
export function useSubmissions({ projectId, initialSubmissions }: UseSubmissionsProps) {
    const { user } = useUser();
    const { session } = useSession();
    const [submissions, setSubmissions] = useState<any[]>(initialSubmissions);

    useEffect(() => {
        if (!user || !session) return;

        const setupRealtime = async () => {
            const token = await session.getToken({ template: 'supabase' });
            console.log('Token:', token);
            if (!token) {
                console.error('Failed to get Clerk token for Supabase');
                return;
            }

            const supabase = createRealtimeClient(token);

            const channel = supabase
                .channel(`submissions:${projectId}`)
                .on('postgres_changes', {
                    event: 'INSERT',
                    schema: 'public',
                    table: 'submissions'
                }, (payload) => {
                    console.log('Submission received', payload);
                    const newSubmission = payload.new;
                    if (newSubmission?.status === 'pending' || newSubmission?.status === 'submitted') {
                        setSubmissions((current) => [...current, newSubmission]);
                    }
                })
                .on('postgres_changes', {
                    event: 'UPDATE',
                    schema: 'public',
                    table: 'submissions'
                }, (payload) => {
                    console.log('Submission updated', payload);
                    const updatedSubmission = payload.new;
                    setSubmissions((current) =>
                        current.map((submission) =>
                            submission.id === updatedSubmission.id ? updatedSubmission : submission
                        )
                    );
                })
                .on('postgres_changes', {
                    event: 'DELETE',
                    schema: 'public',
                    table: 'submissions'
                }, (payload) => {
                    console.log('Submission removed', payload);
                    const deletedSubmission = payload.old;
                    setSubmissions((current) =>
                        current.filter((submission) => submission.id !== deletedSubmission.id)
                    );
                })
                .subscribe((status, err) => {
                    console.log('📡 Subscription status:', status);
                    console.log('📡 Full error object:', err);

                    if (status === 'CHANNEL_ERROR') {
                        console.error('❌ CHANNEL_ERROR details:', {
                            error: err,
                            errorType: typeof err,
                            errorKeys: err ? Object.keys(err) : 'no error object'
                        });
                    }
                });

            return () => {
                channel.unsubscribe();
            };
        };

        setupRealtime();
    }, [user?.id, projectId, session?.id])

    return { submissions }
}


My realtime client:
export const createRealtimeClient = (token: string) => {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      auth: {
        persistSession: false
      },
      global: {
        headers: {
          Authorization: `Bearer ${token}`
        }
      },
      realtime: {
        params: {
          apikey: token,
          access_token: token
        }
      }
    }
  )
}
Was this page helpful?