How to Implement Real-Time Features with Supabase Realtime Subscriptions
🔍 Want the best deal? Check current prices and availability.
Compare Prices →When you buy through links on our site, we may earn a commission.
Users expect instant feedback: live chat, collaborative editing, real-time dashboards, auction updates. Building these features used to mean wrangling WebSockets, managing state sync, and scaling connections — not trivial for a solo dev or small team.
Supabase Realtime.com/realtime?utm_source=devtoolrank&utm_medium=affiliate) fixes that by layering real-time capabilities directly on top of PostgreSQL. It uses logical replication to listen for database changes and broadcasts them over WebSockets. You get the reliability of your existing database and the flexibility of pub/sub, all without standing up a separate server.
In this tutorial, I’ll walk through exactly how to implement live updates with Supabase Realtime — subscriptions, filters, presence, and broadcasting. We’ll compare it to alternatives, weigh the trade-offs, and decide if it’s the right fit for your next project.
What Supabase Realtime Actually Does
Supabase Realtime is built on three pillars:
- Database changes – Subscribe to inserts, updates, and deletes on any table.
- Presence – Know who is online and sync their cursor, status, or any lightweight state.
- Broadcast – Send arbitrary messages to all clients in a channel without touching the database.
All of these run over a single WebSocket connection per client, managed by the Supabase client library (@supabase/supabase-js).
How it works under the hood
When you enable Realtime for a table (via the Supabase dashboard or SQL), Supabase creates a replication slot in Postgres. Every time a row changes, Postgres writes a WAL (Write-Ahead Log) entry. Realtime’s server picks up that change, transforms it into a JSON payload, and pushes it to all subscribed clients.
This means you’re literally reacting to database transactions — not polling, not faking it with timers. If you can write to Postgres, you can build a real-time app.
Step-by-Step: Subscribing to Database Changes
Let’s start with the most common use case: refreshing a feed or a list when new data arrives.
1. Install and initialize the client
npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
)
2. Subscribe to a table
Suppose we have a todos table. We want to listen for new rows:
const channel = supabase
.channel('todos-list')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'todos' },
(payload) => {
console.log('New todo:', payload.new)
// Update your UI here (e.g., prepend item to list)
}
)
.subscribe()
You can also listen for UPDATE and DELETE by passing the event as an array or using a wildcard.
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'todos' },
(payload) => { / handle all changes / }
)
3. Filter subscriptions with a filter clause
To listen only for rows that match a certain condition — for example, tasks assigned to a specific user — use the filter parameter:
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'todos',
filter: user_id=eq.${userId}
},
(payload) => { ... }
)
The filter syntax is the same as Supabase's REST API. You can use eq, neq, gt, gte, lt, lte, like, in, and is for booleans.
4. Listen to multiple channels
Each channel() call creates a separate topic. You can subscribe to different tables or different filter scopes under the same WebSocket connection:
const channelA = supabase.channel('public-todos').on(...).subscribe()
const channelB = supabase.channel('private-todos').on(...).subscribe()
Both share the same socket, so you don’t waste connections.
Going Beyond Database Changes: Presence & Broadcast
Presence
Presence is perfect for showing who’s typing, who’s viewing the same document, or user online status. Each client sends a small payload (e.g., { user_id, username, cursor_position }) when they join a channel. Supabase Realtime tracks who’s in the channel and notifies all members when someone joins or leaves.
const presenceChannel = supabase.channel('document-123', {
config: {
presence: {
key: currentUser.id,
},
},
})
presenceChannel
.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('Joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('Left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await presenceChannel.track({
user_id: currentUser.id,
online_at: new Date().toISOString(),
})
}
})
Broadcast
Sometimes you don’t want to touch the database at all. For chat messages, cursor movements, or game actions, use broadcast to send ephemeral messages.
const broadcastChannel = supabase.channel('room-42')
// Listen for messages
broadcastChannel.on('broadcast', { event: 'cursor' }, (payload) => {
updateCursor(payload.user_id, payload.x, payload.y)
}).subscribe()
// Send a message
broadcastChannel.send({
type: 'broadcast',
event: 'cursor',
payload: { x: 100, y: 200 }
})
Broadcast messages are not persisted — they fly over WebSockets and disappear. If you need history, write the event to a table separately.
Scaling and Pricing Reality
Supabase’s free tier (500 concurrent connections, 2 million real-time messages/day) is generous for a side project. But once you need more, pricing jumps quickly.
| Feature | Supabase Free | Supabase Pro ($25/mo) | Supabase Team ($599/mo) |
|---|---|---|---|
| Concurrent connections | 500 | 500 | 5,000 |
| Real-time messages/day | 2 million | 5 million | 50 million |
| Replication slots | 1 | 5 | 10 |
| Dedicated infrastructure | Shared | Shared | Dedicated |
Compare that to alternatives:
| Tool | Free Tier | Price for 1,000 connections | Notes |
|---|---|---|---|
| Supabase Realtime | 500 conns, 2M msg/day | $25/mo (Pro) | Tight Postgres integration |
| Firebase Realtime Database | 100 conns, 1GB stored | Pay-as-you-go (~$25/mo) | NoSQL, global sync |
| Ably | 1M messages/month, 200 conns | $49/mo (1K conns) | Fully managed pub/sub with presence |
| Socket.io + Postgres (self-hosted) | Unlimited (on your infra) | Server cost (e.g., $10/mo VPS) | You maintain scaling and reliability |
| Pusher | 200K messages/day, 200 conns | $99/mo (1K conns) | Mature but pricey for volume |
If your app is already on Postgres, Supabase cuts complexity. If you’re building a high‑frequency trading dashboard or a massive multiplayer game, you might want a dedicated pub/sub service like Ably.
Pros of Supabase Realtime
- Zero extra infrastructure – Your database is already the source of truth. No separate Pub/Sub server to manage.
- Postgres-native – You can use Row Level Security (RLS) policies, triggers, and existing queries. Changes are reflected in real time without extra code.
- Good free tier – 500 concurrent connections and 2M messages/day cover most prototypes and early‑stage products.
- Client library is straightforward – Simple
.on()and.channel()API. Works with React, Vue, Svelte, or vanilla JS. - Presence and broadcast built in – No need for a third‑party service for these features.
Cons of Supabase Realtime
- Limited message throughput – 5M messages/day on Pro may not suffice for chat apps with thousands of active users. Each keystroke or cursor move counts.
- No built‑in message persistence for broadcast – Chat history requires you to write a separate DB insert and handle conflict resolution.
- Scaling can get expensive – The jump from Pro ($25) to Team ($599) is huge. For mid‑scale apps, you outgrow Pro fast.
- Filtering is limited to a single column filter – You can’t do complex WHERE clauses or joins in the subscription. You need to listen to a broader scope and filter on the client.
- Beta‑level features – Presence and broadcast are still labelled as “beta” (as of mid‑2025). Expect occasional quirks.
- Dependency on Supabase hosting – You can self‑host the Realtime server, but it’s not as polished as the managed version.
Final Verdict
Is Supabase Realtime right for you? That depends on your project’s scale and architecture.
If you already use Supabase for your database and auth, and you need real‑time features that mirror your database changes (like a live‑updating dashboard, collaborative document editing, or a notification feed), go with Supabase Realtime. It lets you avoid the operational overhead of a separate pub/sub service and keeps everything in one platform.
If your app requires high‑frequency broadcasts (e.g., real‑time multiplayer, stock tickers, or chat with hundreds of messages per second) and you can tolerate a non‑SQL backend, consider Ably or a raw WebSocket setup with Socket.io. They give you more control and better scaling characteristics.
For solo devs and indie hackers building an MVP or a small community app, Supabase Realtime is a no‑brainer. The free tier + Pro are enough for thousands of users. I’ve used it in production for a collaborative whiteboarding tool and it performed reliably.
Verdict: If your real‑time needs revolve around database changes and you want the tightest Postgres integration, Supabase Realtime wins. For pure pub/sub at higher volumes, look elsewhere.
FAQ
Q: Can I use Supabase Realtime without the Supabase database?
No – the database change subscriptions depend on Postgres replication slots. However, you can use the broadcast and presence features with any Supabase project, even if you don’t store data in Postgres tables (but the project itself still uses Postgres under the hood).
Q: Does Supabase Realtime work with Row Level Security?
Yes. Your RLS policies are enforced when changes are replicated. A client will only receive changes for rows they are authorized to see, as long as the subscription is initiated with the user’s JWT.
Q: How many concurrent connections can one channel handle?
Supabase doesn’t document a strict per‑channel limit. In practice, we’ve run 500 users on a single channel without issues. The overall connection limit (500 on free, 500 on Pro) is the main constraint.
Q: Can I filter by multiple conditions?
Currently, the filter parameter accepts only one filter. To listen to changes matching multiple conditions, you’ll need to subscribe to the whole table and apply client‑side filtering, or use a separate channel per filter set.
Q: Is there a way to get the “old” row value on updates?
Yes – the payload contains both new and old (the database row before the change). This works when you subscribe to UPDATE or DELETE events.
Q: What happens if the WebSocket disconnects?
The client library automatically reconnects and replays missed messages based on a “last seen” timestamp. Under heavy load, you might lose a few messages if the connection drops for a long time. For critical applications, implement idempotent logic or a fallback polling mechanism.
🔍 Want the best deal? Check current prices and availability.
Compare Prices →