Supabase Realtime is the broadcast layer that lets your app subscribe to database changes and receive updates over a WebSocket. It’s a real piece of infrastructure — well-built, scalable, and free up to a generous limit. It’s also, in our experience auditing client sites, the most over-used Supabase feature. Most SMB sites that use Realtime are paying for complexity they don’t need.
What Realtime actually does
Three things. First, Postgres Changes: subscribe to INSERT/UPDATE/DELETE events on a table and receive them in your client. Second, Broadcast: send arbitrary messages to all subscribers of a channel (useful for chat, cursor presence, ephemeral state). Third, Presence: track which clients are connected to a channel right now (useful for online indicators).
All three run over WebSockets. The Supabase client SDK handles reconnection, replay, and authentication.
When Realtime is the right tool
- Collaborative editing: multiple users editing the same document, seeing each other’s changes in real time.
- Live dashboards: ops dashboards where metrics need to update without a page refresh.
- Chat features: instant messaging between users.
- Multi-device sync: a user has the app open on phone + laptop, changes on one need to reflect on the other.
- Notifications: surface a new event to a logged-in user immediately.
When it’s overkill
Most marketing sites. Most admin dashboards. Most lead inboxes. A 30-second polling interval is usually fine for the use cases that look real-time but aren’t — and polling is dramatically simpler to debug, monitor, and scale.
We had a client request a real-time lead notification feature: when a new lead came in, the team should see it immediately on the admin page. We implemented it with Realtime. Three months later we ripped it out and replaced it with a 15-second polling fetch. The Realtime version had two production issues (a dropped WebSocket showed no leads at all for 6 hours; an RLS policy bug let one tenant see another’s leads briefly). The polling version has had zero issues in 18 months.
The Postgres Changes pattern
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export function LiveLeadFeed() {
const [leads, setLeads] = useState<unknown[]>([]);
useEffect(() => {
const channel = supabase
.channel("leads")
.on(
"postgres_changes",
{ event: "INSERT", schema: "public", table: "submissions" },
(payload) => setLeads((prev) => [payload.new, ...prev])
)
.subscribe();
return () => { supabase.removeChannel(channel); };
}, []);
return <ul>{leads.map((l: any) => <li key={l.id}>{l.name}</li>)}</ul>;
}RLS still applies — and that’s a trap
Realtime respects RLS policies. If your policy says “only authenticated users from site X can read row Y,” Realtime enforces that. But the policy is evaluated against the user’s JWT at subscribe time. If the JWT expires mid-session, the subscription quietly stops delivering events. If the user logs out, the subscription becomes a no-op without an obvious error. Both have bitten us.
Broadcast: when you don’t need persistence
Broadcast is for ephemeral state — cursor position, “user is typing,” a temporary notification. Messages aren’t persisted. If a client is offline when the message is sent, it never sees the message.
For chat where messages need to survive disconnect, use a regular `messages` table + Postgres Changes subscription. The DB is the source of truth; Realtime is the delivery mechanism.
Presence: where it earns its keep
Presence tracks who’s connected to a channel right now. We use it on one client’s collaborative dashboard so users see “3 team members viewing this report.” It’s the only piece of Realtime we use in production today.
Cost
Supabase Pro includes 500 concurrent realtime connections and 2M messages per month. Plenty for an SMB. If you’re building chat at scale or a multi-thousand-user collaborative app, you’ll exceed it — at which point Realtime is still 1/5 the cost of building the same on Pusher or Ably.
The polling-first principle
Before reaching for Realtime, ask: how stale can the data be before users notice? If the answer is anything more than 5 seconds, poll. A 15-second polling interval costs almost nothing in CPU or bandwidth, never has WebSocket drop bugs, and is trivially debuggable. The “feels real-time” bar is usually 5–15 seconds — only true real-time collaboration needs anything tighter.
When you do use Realtime, instrument it
Subscribe to your own channel’s health events. Log every reconnect, every error, every subscription state change. Without instrumentation, a broken Realtime is invisible — the page just stops updating. We pipe Realtime events into GA4 as custom events so we can see in aggregate whether subscriptions are stable across users.
How this lands across FH client work
Across the FH client book we use Realtime in exactly one place: a Presence indicator on one client’s shared dashboard. Everything else — lead feeds, project status pages, admin dashboards — uses polling on 5-to-15-second intervals. Code is simpler, monitoring is simpler, ops incidents are zero. If you’ve been talked into Realtime for a feature that really only needs polling, book a consultation — the rip-out is usually a clean win.