How to Implement Real-Time Features with Supabase Realtime Subscriptions

Tutorial · 11 min read 🔄 Affiliate Links

🔍 Want the best deal? Check current prices and availability.

Compare Prices →

When you buy through links on our site, we may earn a commission.

Real-time features used to be a headache. You’d either roll your own WebSocket server, pay a fortune for a hosted service, or glue together a patchwork of polling intervals that made your database cry. Then Supabase came along with its “Realtime” extension, and suddenly adding live updates felt almost too easy.

In this tutorial, I’ll walk you through exactly how to use Supabase Realtime subscriptions – from the basics of listening to database changes, to more advanced patterns like presence and broadcast. We’ll look at real code examples, compare pricing against alternatives, and I’ll tell you where Supabase’s approach falls short (because nothing is perfect).

If you’re building a chat, a collaborative editor, a live dashboard, or any app that needs to push updates to users instantly, this is for you.


What Is Supabase Realtime?

Supabase Realtime is a WebSocket-based layer that sits on top of your PostgreSQL database. It uses PostgreSQL’s built-in replication feature (pgoutput plugin) to listen for INSERT, UPDATE, DELETE, and TRUNCATE events on specific tables. When a change happens, Supabase serializes the change and broadcasts it to all connected clients via WebSockets.

That means you don’t need a separate message queue or a dedicated real-time server. Your database is the single source of truth, and Supabase handles the fan-out.

There are three types of subscriptions:

  • Realtime (database changes) – Listen for row-level changes.
  • Presence – Track who’s online (syncs state across clients).
  • Broadcast – Send arbitrary messages between clients (low-latency, no DB involved).

We’ll cover all three.


Setting Up Supabase Realtime

First, you need a Supabase project. If you don’t have one, create a free account at Supabase (the free tier includes 2 million real-time messages per month – more than enough for prototyping).

Enable Realtime on a Table

By default, Realtime is disabled for all tables. You need to enable it per table:

  • Go to your Supabase project dashboard.
  • Navigate to Database > Replication.
  • Under “Source”, enable replication for the table(s) you want to watch.

Alternatively, you can do this via SQL:

alter publication supabase_realtime add table todos;

Now any changes to the todos table will be broadcast.

Install the Client Library

In your frontend app (I’ll use JavaScript/React, but the same concepts apply to any framework):

npm install @supabase/supabase-js

Initialize the client:

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://your-project.supabase.co'

const supabaseAnonKey = 'your-anon-key'

const supabase = createClient(supabaseUrl, supabaseAnonKey)


Basic Realtime Subscription: Listening to Database Changes

Let’s say you have a todos table and you want to update the UI whenever a new todo is added, updated, or deleted.

// Subscribe to changes on the 'todos' table

const subscription = supabase

.channel('todos-changes') // any unique channel name

.on(

'postgres_changes',

{ event: '*', schema: 'public', table: 'todos' },

(payload) => {

console.log('Change received!', payload)

// payload.new -> the new row (for INSERT/UPDATE)

// payload.old -> the old row (for DELETE/UPDATE)

// payload.eventType -> 'INSERT' | 'UPDATE' | 'DELETE'

}

)

.subscribe()

You can also filter by a specific event type:

.on(

'postgres_changes',

{ event: 'INSERT', schema: 'public', table: 'todos' },

(payload) => { / handle insert / }

)

Or filter by a column value using a filter:

.on(

'postgres_changes',

{

event: '*',

schema: 'public',

table: 'todos',

filter: 'user_id=eq.123' // only changes where user_id = 123

},

callback

)

Important: The filter uses the Realtime “row-level security” syntax – it’s not a raw SQL filter. You can use eq, neq, gt, gte, lt, lte, in, is, like, ilike.

Cleanup

Don’t forget to unsubscribe when your component unmounts to avoid memory leaks:

subscription.unsubscribe()


Presence: Who’s Online?

Presence is great for collaborative features – seeing who else is viewing the same document, typing in a chat, or editing a field.

Supabase Presence syncs a state object across all clients subscribed to the same channel. Each client can set its own state (e.g., { online_at: Date.now(), cursor: { x, y } }) and the server broadcasts it to everyone else.

const channel = supabase.channel('room-1')

// Track own presence

const presenceTrack = await channel.track({

user_id: currentUser.id,

user_name: currentUser.name,

online_at: new Date().toISOString()

})

// Listen for presence changes

channel.on('presence', { event: 'sync' }, () => {

const state = channel.presenceState()

console.log('Online users:', state)

// state is an object keyed by user_id, values are arrays of presence states

})

channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {

console.log(${key} joined, newPresences)

})

channel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {

console.log(${key} left, leftPresences)

})

channel.subscribe()

Presence state is automatically cleaned when a client disconnects (within a few seconds). You can also call channel.untrack() to manually remove your presence.


Broadcast: Low-Latency Peer-to-Peer Messages

Broadcast lets you send arbitrary JSON messages between clients on the same channel, bypassing the database entirely. This is useful for things like cursor movements, typing indicators, or game moves where you don’t need persistence.

const channel = supabase.channel('game-room', {

// Broadcast is enabled by default, but you can configure selfBroadcast

config: { broadcast: { self: true } } // also receive your own messages

})

// Listen for broadcast messages

channel.on('broadcast', { event: 'move' }, ({ payload }) => {

console.log('Move received:', payload)

// update game state

})

channel.subscribe()

// Send a broadcast

channel.send({

type: 'broadcast',

event: 'move',

payload: { x: 10, y: 20 }

})

Broadcast messages are not stored anywhere – they’re fire-and-forget. Use them for transient data.


Putting It All Together: A Live Chat Example

Let’s build a minimal chat component that uses both database changes (for storing messages) and presence (to show who’s online).

import { useEffect, useState } from 'react'

import { supabase } from './supabaseClient'

function ChatRoom({ roomId, userId }) {

const [messages, setMessages] = useState([])

const [presence, setPresence] = useState({})

const channel = supabase.channel(room-${roomId})

useEffect(() => {

// 1. Load initial messages

loadMessages()

// 2. Subscribe to new messages via Realtime

channel

.on('postgres_changes',

{ event: 'INSERT', schema: 'public', table: 'messages', filter: room_id=eq.${roomId} },

(payload) => {

setMessages(prev => [...prev, payload.new])

}

)

.on('presence', { event: 'sync' }, () => {

setPresence(channel.presenceState())

})

.subscribe(async (status) => {

if (status === 'SUBSCRIBED') {

await channel.track({

user_id: userId,

user_name: getUserName(userId)

})

}

})

return () => {

channel.unsubscribe()

}

}, [roomId])

async function loadMessages() {

const { data } = await supabase

.from('messages')

.select('*')

.eq('room_id', roomId)

.order('created_at', { ascending: true })

setMessages(data || [])

}

async function sendMessage(text) {

await supabase.from('messages').insert({

room_id: roomId,

user_id: userId,

text,

created_at: new Date().toISOString()

})

}

// ... render UI

}

That’s it. The database insert triggers the Realtime event, which updates the UI for all connected clients. Presence updates show who’s online.


Comparison Table: Supabase Realtime vs Alternatives

FeatureSupabase RealtimeFirebase Realtime DatabaseAblyPusher
Database integrationPostgreSQL (native)NoSQL (Firebase DB)None (own pub/sub)None (own pub/sub)
Realtime messages (free tier)2 million/month1 GB stored (no msg limit)200k messages/month200k messages/day
PresenceYes (built-in)Yes (via .info/connected)Yes (via channel state)Yes (via channel presence)
BroadcastYesYesYesYes
Latency~50-100ms (varies)~50-200ms~10-50ms~20-50ms
Scaling costPay per message (beyond free)Pay per storage & bandwidthPay per message (higher)Pay per connection & messages
Self-hostableYes (open-source)NoNoNo
Server-side filteringYes (via row-level filters)LimitedYesLimited

Winner depends on your stack. If you’re already using Postgres, Supabase is a no-brainer. If you need ultra-low latency (<20ms) for a game, Ably might be better. Firebase is great if you’re all-in on Google Cloud and prefer NoSQL.


Pricing (Supabase Realtime)

Supabase’s pricing for Realtime is based on the number of messages:

  • Free Plan: 2 million messages/month, 200 concurrent connections
  • Pro Plan ($25/month): 2 million messages/month (same as free, but includes other Pro features like database backups and higher limits)
  • Team Plan ($125/month): 40 million messages/month, 500 concurrent connections
  • Enterprise: Custom

Messages are counted per “event” – each database change sent to one client is one message. So if 10 clients are subscribed and a row changes, that’s 10 messages.

If you’re building a small app, the free tier will last you a long time. A typical chat app with 1000 active users might send a few hundred thousand messages per month.

Check Supabase Pricing ->


Pros and Cons of Supabase Realtime

Pros

  • Zero infrastructure – No separate WebSocket server to manage.
  • PostgreSQL native – Your database changes become real-time events automatically. No duplication of data.
  • Open source – You can self-host the Realtime server if needed.
  • Row-level filtering – Filter events by column values, which reduces unnecessary data transfer.
  • Combines presence & broadcast – Everything in one SDK, no extra services.
  • Generous free tier – 2 million messages/month is plenty for most MVPs and side projects.

Cons

  • Latency isn’t the lowest – Because it goes through PostgreSQL replication, there’s a small delay (50-100ms typically). For most apps it’s fine, but for real-time gaming or financial tickers, you’ll want something faster.
  • Message count can add up – If you have many clients watching many tables, the free tier disappears fast. Monitor your usage carefully.
  • Limited to PostgreSQL – If you’re using MySQL or another database, you can’t use this feature.
  • Filter syntax is limited – You can’t do complex boolean logic (e.g., (status=eq.done AND user_id=eq.123) OR priority=eq.high). Filters are single-column only.
  • No guaranteed ordering across clients – Events are processed in the order they arrive from the database, but due to network jitter, clients may see them slightly out of order. For critical ordering (e.g., chat messages), rely on the database timestamp.
  • Presence state is ephemeral – If the Realtime server restarts, presence state resets. Clients need to re-track.

When Should You Use Supabase Realtime?

Use it when:

  • You’re already using Supabase (or PostgreSQL) as your primary database.
  • You need to sync database changes to clients – like live dashboards, collaborative editing, or notifications.
  • You want a simple, all-in-one solution without managing extra services.

Skip it if:

  • You need sub-50ms latency for every message (e.g., multiplayer game).
  • You’re on a non-Postgres database and don’t want to migrate.
  • You have very high message volumes (millions per day) and want to keep costs predictable – then consider a dedicated pub/sub service like Ably.

Final Verdict

Supabase Realtime is the easiest way to add real-time features to a PostgreSQL-backed application. It’s not the fastest or the cheapest at scale, but for 90% of use cases (chat, notifications, live feeds, collaborative tools), it’s more than enough.

The tight integration with the database means you don’t have to maintain state in two places. The presence and broadcast features are a nice bonus that cover most real-time patterns.

If you’re building a new app today and you’re even slightly considering PostgreSQL, start with Supabase Realtime. You can always swap out the real-time layer later if you outgrow it – but you probably won’t.

Verdict: Highly recommended for PostgreSQL users. 8.5/10.


Frequently Asked Questions

Q: Does Supabase Realtime work with Row-Level Security (RLS)?

A: Yes, but RLS is applied on the database query (SELECT/INSERT/UPDATE). Realtime events are broadcast after the database change is committed, so RLS does not filter real-time messages. You must use the filter parameter to restrict who receives changes, or implement client-side filtering.

Q: Can I use Realtime with a self-hosted Supabase?

A: Yes, the Realtime server is open source. You can run it alongside your own Postgres instance. See the Supabase Realtime GitHub repo for instructions.

Q: What happens if a client goes offline?

A: Supabase Realtime does not persist messages for offline clients. If you need offline support, you’ll need to cache data locally and reconcile when the client reconnects. The client library automatically reconnects and resumes subscriptions.

Q: How do I authenticate Realtime connections?

A: Use the same supabase.auth session. The Realtime client uses the anon key, but you can restrict access using database RLS policies on the tables you’re subscribing to. For presence and broadcast, you can validate user identity via the track payload.

Q: Is there a limit on the number of channels?

A: Not explicitly, but each channel consumes a WebSocket connection. The free tier limits concurrent connections to 200. On Pro, it’s 200 as well (but you can open a support ticket to increase it). For many channels, you can reuse a single channel with different event types.

Q: Can I use Supabase Realtime in a React Native app?

A: Yes, the @supabase/supabase-js library works in React Native. However, the WebSocket implementation may require polyfills on older devices. Supabase also provides a dedicated @supabase/realtime-js package if you need finer control.


Ready to build? Create a free Supabase project and start adding live updates in minutes. Get started with Supabase ->

🔍 Want the best deal? Check current prices and availability.

Compare Prices →
D

Dev Tool Rank Editorial Team

We're a team of tech enthusiasts who test and review tools so you don't have to. Our reviews are independent — we only recommend what we'd actually use ourselves.