You've decided your app needs real-time updates. Data changes on the server, and users see it instantly without refreshing. Three patterns exist: raw WebSocket, Server-Sent Events (SSE), and Supabase Realtime. If you've lived under a rock, here's the 30-second recap. WebSocket opens a persistent bidirectional tunnel between client and server — perfect for chat, live games, or anything that needs server→client and client→server messages. SSE is simpler: client opens a one-way stream from server, and the server pushes data down. Perfect for notifications, live pricing updates, or AI token streaming. Supabase Realtime is a managed layer that wraps WebSocket and gives you broadcast, presence, and database change subscriptions with zero infra. The internet collectively lost its mind over "serverless everything", but the real win is: you pick the abstraction level that matches your problem. This post covers all three, decision tree, and real patterns from production Velocity dashboards.
The Three Approaches Compared
Real-time data boils down to two questions: who initiates messages, and who manages the infrastructure?
Raw WebSocket: Bidirectional, Full Control
WebSocket is a browser standard that upgrades an HTTP connection to a persistent, bidirectional socket. The server keeps it open, and messages can flow both ways at any time with zero latency. You own the connection, you own the server, you own scaling. Use WebSocket when you need: true bidirectionality (client sends, server sends, both without asking), microsecond-level latency (think high-frequency trading), or custom binary protocols (game state, sensor data). The catch: you're running a WebSocket server (Node.js, Rust, Go, take your pick), you're handling reconnections, you're scaling horizontally, you're debugging half-closed sockets at 2am. WebSocket is powerful but comes with infrastructure tax.
Server-Sent Events: One-Way, Browser-Native, Simple
SSE is the forgotten API. It lives in the browser as EventSource and lets the server push messages down an open HTTP connection. Unlike WebSocket, it's one-way: server→client only. The client can't send messages back (well, it can send separate HTTP requests, but that's not SSE). SSE is perfect for: live streams (like Claude's token streaming), push notifications, live dashboard updates from a data source you control. The magic: it's HTTP under the hood, so it works with any web server (Express, Python, ASP.NET) and respects CORS natively. Reconnection is automatic — the browser's EventSource API retries if the connection drops. For 80% of real-time features, SSE is simpler than WebSocket because you don't need bidirectional traffic.
Supabase Realtime: Managed WebSocket with Database Awareness
Supabase Realtime is a hosted layer that abstracts away WebSocket management. It ships with every Supabase database and gives you three primitives: broadcast (fire-and-forget messages), presence (track online users), and postgres_changes (stream database row updates). You open a single persistent connection to Supabase's Realtime server, and it handles reconnection, scaling, and message routing for you. Use Supabase Realtime when: you need bidirectional messaging but don't want to manage a server, you're already using Supabase for auth/database, you want RLS-enforced subscriptions (users see only their data), or you need tight database integration. Pricing is per message, not per connection, so it scales with usage, not concurrent users. For most SaaS, Supabase Realtime is the goldilocks option: managed infrastructure + database awareness + zero setup.
Decision Tree: Which One For Your Feature
Use Supabase Realtime if: You need bidirectional real-time data and you're already on Supabase. Example: dashboards, collaborative editing, live notifications tied to database rows. This is the default for Velocity dashboards and Aidxn projects. It's managed (no server to run), RLS-scoped (automatic permission checking), and free for small projects.
Use SSE if: You need one-way server-to-client streaming and want maximum simplicity. Example: AI token streaming (like an LLM chat endpoint), live price tickers from your backend, or system notifications. SSE is HTTP-native, works with any backend, and is trivial to implement. Clients reconnect automatically.
Use raw WebSocket if: You control both client and server infrastructure, you need bidirectional messaging at scale, and you're willing to manage server code. Example: multiplayer games, real-time whiteboard/collaborative drawing, or a custom trading terminal. Only pick this if Supabase Realtime's latency (50–200ms) or message pricing doesn't fit, or you need binary protocols.
Supabase Realtime Pattern: Broadcast + Presence + postgres_changes
Here's how to wire Supabase Realtime in a React component (the Velocity dashboard pattern):
import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
export function LiveDashboard({ orgId, userId }) {
const [onlineUsers, setOnlineUsers] = useState([]);
const [kpis, setKpis] = useState([]);
useEffect(() => {
const channel = supabase
.channel(`org-${orgId}`, {
config: { presence: { key: `user-${userId}` } }
})
// Track presence: who's viewing now
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
setOnlineUsers(Object.values(state).flat());
})
// Stream database changes: live KPIs
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'daily_metrics',
filter: `organization_id=eq.${orgId}`
},
(payload) => {
setKpis((prev) => [...prev, payload.new]);
}
)
// Broadcast events: UI state
.on('broadcast', { event: 'export_done' }, (payload) => {
console.log(`${payload.user_name} exported the report`);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Announce you're here
await channel.track({
user_id: userId,
email: userEmail,
color: '#3b82f6'
});
}
});
return () => {
channel.unsubscribe();
};
}, [orgId, userId]);
return (
Online now: {onlineUsers.map(u => u.email).join(', ')}
{kpis.map((kpi) => (
{kpi.revenue} → {kpi.metric}
))}
);
}
One subscription, three event types. When a KPI is inserted (postgres_changes), your chart updates. When a colleague joins (presence), their name appears. When data is exported (broadcast), everyone sees it. Supabase routes everything over one WebSocket, so latency is low and scaling is Supabase's problem.
SSE Pattern: Streaming AI Responses
Here's how to stream tokens from an LLM API using SSE (the common pattern for Claude token streaming):
// Server (Node.js + Express)
app.get('/api/stream-response', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const response = await fetch('https://api.anthropic.com/messages', {
method: 'POST',
body: JSON.stringify({
model: 'claude-opus-4-1',
messages: [{ role: 'user', content: 'Explain real-time patterns...' }],
stream: true
})
});
// Pipe the streaming response back to client
for await (const chunk of response.body) {
const line = chunk.toString();
res.write(`data: ${line}\n\n`);
}
res.end();
});
// Client (React)
export function AiStreamingChat() {
const [response, setResponse] = useState('');
const handleStream = () => {
const eventSource = new EventSource('/api/stream-response');
eventSource.onmessage = (event) => {
const token = JSON.parse(event.data)?.content;
setResponse((prev) => prev + token);
};
eventSource.onerror = () => {
eventSource.close();
};
};
return (
{response}
);
}
SSE is trivial: set the Content-Type header to text/event-stream, write lines as data: {message}\n\n, and the browser's EventSource API handles reconnection. No server state, no WebSocket library, just HTTP streams.
Raw WebSocket Pattern: Multiplayer Game State
Raw WebSocket is useful when you need bidirectional, bidi at scale, and custom binary protocols. Here's a simplified example:
// Server (Node.js + ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map();
wss.on('connection', (ws) => {
const clientId = Math.random().toString(36);
clients.set(clientId, ws);
ws.on('message', (message) => {
const { type, position } = JSON.parse(message);
// Broadcast position update to all other clients
clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
clientId,
type: 'player_moved',
position
}));
}
});
});
ws.on('close', () => {
clients.delete(clientId);
// Notify others that player left
clients.forEach((client) => {
client.send(JSON.stringify({
clientId,
type: 'player_left'
}));
});
});
});
// Client (React)
export function MultiplayerGame() {
const [otherPlayers, setOtherPlayers] = useState({});
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
const { clientId, type, position } = JSON.parse(event.data);
if (type === 'player_moved') {
setOtherPlayers((prev) => ({
...prev,
[clientId]: position
}));
}
};
return () => ws.close();
}, []);
return (
);
}
Raw WebSocket gives you full control: define your message types, implement custom routing, scale with multiple servers + a message queue. The trade-off is you manage every detail.
Latency, Pricing, and Scaling Comparison
Latency: Raw WebSocket (<50ms), Supabase Realtime (50–200ms, database queries add time), SSE (50–500ms depending on backend). Raw WebSocket wins on speed but only if you're optimizing for microseconds. For dashboards and notifications, the differences are invisible.
Pricing: Raw WebSocket (server cost + bandwidth), Supabase Realtime (per-message pricing, ~$1/M messages), SSE (server cost + bandwidth, often bundled with backend). Supabase scales with usage, not concurrent connections. If you have 100 users checking a dashboard hourly, you're in the cents-per-day range.
Scaling: Raw WebSocket requires load balancers, sticky sessions, and a message queue (Redis, RabbitMQ) to fan messages across servers. Supabase Realtime scaling is handled for you. SSE is simple if your backend is stateless; complex if you need to route messages across multiple server instances.
The Aidxn Rule: 95% of SaaS Uses Supabase Realtime
For most web applications, Supabase Realtime is the right tool. It's managed (no server to run), RLS-scoped (automatic permission checking), and free for small projects. Use it for dashboards, collaborative editing, notifications, and anything tied to your database. The latency (50–200ms) is fine for human-scale interactions. Only reach for SSE or raw WebSocket when Supabase doesn't fit: SSE for AI streaming or one-way data flows, raw WebSocket for games or microsecond trading. Velocity X uses Supabase Realtime for all dashboard features — broadcast for notifications, presence for online tracking, postgres_changes for live KPIs. Zero custom server code.
Six FAQs
Can I use WebSocket in Next.js API routes?
No. Next.js API routes are HTTP-only and don't support WebSocket upgrades. You'd need a separate WebSocket server (like ws or Socket.IO running on its own port). Supabase Realtime sidesteps this — just use the client library, no server code needed.
Is SSE safe for sensitive data?
SSE travels over HTTPS like any HTTP request, so it's encrypted in transit. Implement authentication (JWT tokens) and authorization (RLS policies if using Supabase) to restrict who receives what data. The one-way nature of SSE doesn't make it less or more secure than WebSocket — it's how you authenticate that matters.
What happens if a Supabase Realtime connection drops?
The client library reconnects automatically. 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 re-subscribe. For critical data, use postgres_changes instead of broadcast so updates persist in the database.
Can I use raw WebSocket with serverless (Lambda, Functions, etc.)?
Serverless platforms don't keep connections open. WebSocket needs a persistent server, so Lambda and Cloud Functions won't work. Supabase Realtime or managed WebSocket services (like AWS AppSync) are your options.
How do I rate-limit or throttle messages?
Supabase Realtime has built-in rate limiting (configurable per message type). For SSE, throttle on the server side before writing to the response. For raw WebSocket, implement throttling in your server code (e.g., allow max 10 messages/second per client). For high-frequency data (like stock prices), aggregate updates server-side and send every 100ms instead of every tick.
Should I use Socket.IO instead of raw WebSocket?
Socket.IO is a wrapper around WebSocket that adds fallbacks (polling) and convenience features (rooms, namespaces, auto-reconnection). It's useful if you need cross-browser support for very old clients (IE 9) or need easy room-based broadcasting. For modern apps, raw WebSocket or Supabase Realtime is simpler. Socket.IO adds overhead and requires a server — not worth it for most SaaS.
The Bottom Line
Pick the abstraction that matches your problem. Supabase Realtime for 95% of SaaS (dashboards, notifications, collaborative editing) — it's managed, RLS-scoped, and costs almost nothing. SSE for one-way server-to-client streaming (like AI tokens or price tickers) — it's trivial to implement and works with any backend. Raw WebSocket only when you control both client and server, need microsecond latency, or are building a multiplayer game. For deep Realtime patterns and database-driven subscriptions, see Supabase Realtime — Broadcast, Presence, and DB Changes. Ready to ship? Start with Supabase, measure latency, and iterate. And check Aidxn Design pricing for Realtime scaling numbers on your tier.