Most modern dashboards need real-time updates. Velocity X users watch live revenue, see when colleagues join a room, and collaborate on spreadsheets — all without refreshing. If you're using traditional REST polling or managing your own WebSocket server, you're burning CPU cycles and adding latency. Supabase Realtime ships three built-in primitives: broadcast (send arbitrary messages to a channel), presence (track who's online), and postgres_changes (stream database row updates). They all run on the same managed WebSocket infrastructure — no server, no scaling headaches. Wire them together and you get reactive dashboards, real-time collaboration, and live presence without leaving Postgres. This is how production Velocity dashboards stay responsive: subscribe to changes at the database layer, broadcast custom events for UI state, and track presence for user awareness, all for the cost of a database connection.
Three Realtime Primitives: Broadcast, Presence, postgres_changes
Supabase Realtime is a managed WebSocket layer sitting between your Postgres database and your client. It exposes three APIs: broadcast sends fire-and-forget messages to a named channel (think Slack rooms), presence announces which users are subscribed to a channel right now, and postgres_changes streams actual database changes (INSERT, UPDATE, DELETE) as they happen. All three use the same connection, so one subscription can listen to multiple primitives at once. They're also independent — use just broadcast for a chat app, just presence for a collaborative editor, or all three for a full-featured dashboard.
Broadcast: Fire-and-Forget Messages (No Data Persistence)
Broadcast is the lightest primitive. You send an arbitrary JSON message to a channel, and everyone subscribed to that channel receives it instantly. The message doesn't get stored — it's in-flight only. Use broadcast for: user-initiated events that don't need to be replayed (like "I just liked this"), UI state changes that are ephemeral (like "I expanded this menu"), or notifications that are time-sensitive. If someone isn't subscribed when the message fires, they miss it. That's the trade-off for speed.
Presence: Who's Online Right Now
Presence automatically tracks which users are subscribed to a channel. When you subscribe, your presence state is broadcast to everyone on the channel. When you disconnect, you're removed from the presence list. Each presence entry can carry metadata (like `cursor_x`, `cursor_y`, or `editing_cell`). Use presence for: showing live cursor positions, displaying "user X is viewing this page", or building real-time collaboration awareness. Presence is always tied to a connection — it doesn't outlive the WebSocket.
postgres_changes: Database Row Updates Stream
postgres_changes listens to actual Postgres changes. Subscribe to a table (or a specific row with a WHERE filter), and you get a real-time event for every INSERT, UPDATE, DELETE that matches your filter. Unlike broadcast, these events are anchored to the database, so you can use RLS to filter by user. Use postgres_changes for: keeping a dashboard in sync with the database, building reactive forms that update when other users change data, or triggering side effects when a row changes. postgres_changes is the most powerful because it's database-aware and RLS-enforced.
Decision Tree: Broadcast vs postgres_changes
Choosing between broadcast and postgres_changes is the biggest decision in Realtime architecture. Here's a mental model:
Use broadcast if: The event is UI-driven and doesn't represent a database change. Example: a user clicked a button, expanded a menu, or moved their cursor. The event is ephemeral — you don't need it replayed if someone joins late. You want zero latency and don't care about RLS.
Use postgres_changes if: The change is in the database. Example: a row was updated, a record was created. You need the change to persist and be replayed for new connections. You want RLS-enforced filtering so users only see changes they're allowed to see. The change might come from outside the real-time channel (like a cron job or API call) and you still want the UI to react.
Use both if: Velocity X uses this hybrid pattern. Track presence (broadcast side-effect: show online users). Stream KPI updates from the database (postgres_changes on the `daily_metrics` table). Send UI events like "user just exported data" (broadcast). Wire them all to the same channel and your dashboard stays fully reactive.
Broadcast Pattern: Real-Time Cursors and Notifications
Here's a real pattern from Velocity X dashboards: show live cursor positions when multiple analysts are viewing the same report.
// Client: Subscribe to broadcast channel and track cursors
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// Listen to cursor movements
const channel = supabase
.channel('report-123')
.on('broadcast', { event: 'cursor_move' }, (payload) => {
console.log(`User ${payload.user_id} moved cursor to`, payload.x, payload.y);
updateCursorPosition(payload.user_id, payload.x, payload.y);
})
.subscribe();
// Send cursor position every time the mouse moves
document.addEventListener('mousemove', (e) => {
channel.send({
type: 'broadcast',
event: 'cursor_move',
payload: { user_id: 'user123', x: e.clientX, y: e.clientY }
});
});
Broadcast fires and forgets, so everyone connected at that moment sees it. For notifications that need to persist (like a Slack message), you'd write to the database and use postgres_changes instead.
Presence Pattern: Show Who's Viewing Now
Presence is automatic but requires explicit subscription. When you subscribe, your presence state is added to the channel. When you unsub, you're removed.
// Client: Subscribe and broadcast presence
const channel = supabase
.channel('dashboard-org-123', {
config: {
presence: {
key: `user-${userId}`
}
}
})
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
console.log('Online users:', state);
// state is a map: { 'user-123': [{ user_id, email, color }] }
renderPresence(state);
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log(`User joined:`, newPresences);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log(`User left:`, leftPresences);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Broadcast your presence data
await channel.track({
user_id: userId,
email: userEmail,
color: userColor // for cursor/selection highlight
});
}
});
// On unmount or logout
channel.unsubscribe();
Presence events fire immediately. When you track your state, everyone on the channel sees it. The presence list is stored in memory on the Realtime server, not in the database.
postgres_changes Pattern: Stream Database Updates
postgres_changes listens to actual Postgres changes. Here's how Velocity X dashboards stream live KPI updates:
// Client: Listen for daily_metrics INSERT (new KPI snapshot)
const channel = supabase
.channel('public:daily_metrics')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'daily_metrics',
filter: `organization_id=eq.${orgId}` // RLS: only your org's rows
},
(payload) => {
console.log('New KPI snapshot:', payload.new);
updateDashboardChart(payload.new);
}
)
.subscribe();
// Or listen for updates to a specific row
const userOrdersChannel = supabase
.channel('user-orders')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'orders',
filter: `user_id=eq.${userId}`
},
(payload) => {
console.log('Order updated:', payload.new);
// Old value is in payload.old, new value in payload.new
refetchOrders();
}
)
.subscribe();
The filter syntax is the same as PostgREST. If RLS is enabled, Supabase enforces it — the user only gets changes to rows they can read. This is powerful because your real-time subscriptions are automatically scoped by your database permissions.
Combining All Three: Velocity X Dashboard
Here's how a production dashboard wire all three primitives:
// One channel, three event types
const channel = supabase
.channel('org-dashboard', {
config: {
presence: { key: `user-${userId}` }
}
})
// Listen for presence changes (who's viewing)
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
renderOnlineUsers(state);
})
// Listen for KPI updates (from database)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'daily_metrics',
filter: `organization_id=eq.${orgId}`
},
(payload) => {
updateKPIChart(payload.new);
}
)
// Listen for UI broadcasts (user exported data, etc)
.on('broadcast', { event: 'export_complete' }, (payload) => {
showNotification(`${payload.user_name} exported the report`);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Announce you're here
await channel.track({
user_id: userId,
email: userEmail
});
}
});
One connection, three event streams. When a new KPI comes in (postgres_changes), the chart updates. When a colleague joins (presence), their name appears in the "online now" list. When someone exports (broadcast), everyone sees a toast notification.
RLS + Realtime: Automatic Permission Enforcement
postgres_changes respects RLS policies. If a user doesn't have permission to read a row, they won't get the change event, even if subscribed. This is huge for security: you don't have to manually check permissions in your client code.
-- RLS policy: users only see KPIs from their org
create policy "users see their org's metrics"
on daily_metrics
for select
using (organization_id = auth.uid());
-- Client subscribes to all metrics
const channel = supabase
.channel('daily_metrics')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'daily_metrics'
},
(payload) => {
console.log(payload.new); // User only gets their org's rows
}
)
.subscribe();
Supabase checks the RLS policy before sending the event. If the user doesn't pass the policy, they don't see it. This means your real-time subscriptions are secure by default.
Six FAQs
What's the latency of postgres_changes?
Usually 50–200ms depending on network and server load. It's fast enough for dashboards and collaborative editing, but not for stock tickers at microsecond resolution. For ultra-low-latency use cases, broadcast is faster (just a message relay, no database query involved).
Can I filter postgres_changes by multiple columns?
Yes, the filter syntax supports AND/OR: `organization_id=eq.123&status=eq.pending`. You can also use comparison operators: `amount=gt.1000&created_at=gte.2026-01-01`. The filter is a PostgREST query, so anything PostgREST supports works.
Do I need to keep the subscription open for the entire session?
Yes. Realtime subscriptions are tied to the WebSocket connection. When you unsub or close the connection, you stop getting events. In a React app, subscribe in a useEffect with a cleanup that unsubscribes on unmount.
What happens if my app goes offline and comes back online?
The Realtime client automatically reconnects, but you miss events that happened while offline. If you need a replay, query the database to fetch recent changes (e.g., changes in the last hour) and then subscribe going forward.
Can I subscribe to postgres_changes without a user login?
Only if the table has RLS disabled or has a public SELECT policy. Best practice: require authentication and use RLS policies to scope subscriptions. For public-read tables (like a blog), you can subscribe as the anon user.
How much does Realtime cost?
Realtime is free for small projects. Supabase charges per million messages — for a dashboard with 10 users checking KPIs hourly, you're looking at negligible cost. Check Aidxn Design pricing if you're scaling to thousands of concurrent users.
The Bottom Line
Real-time dashboards are table stakes for modern SaaS. Instead of polling REST endpoints or building a WebSocket server, Supabase Realtime gives you three managed primitives: broadcast for fire-and-forget events, presence for online tracking, and postgres_changes for database-driven updates. Velocity X uses all three: stream live KPIs, show who's viewing, and send notifications — all on one connection. The architecture is transparent, RLS-enforced, and costs almost nothing to run. Ready to ship real-time features? For more on database security and authentication, see Supabase Edge Functions + pg_cron for scheduled tasks that trigger Realtime events.