What is Supabase Realtime?
Supabase Realtime is a globally distributed system that enables real-time communication between connected clients. Built on WebSocket technology, the Realtime service operates as a layer on top of your PostgreSQL database, listening for changes and broadcasting updates to subscribed clients. The system handles the complexity of connection management, message routing, and state synchronization, allowing developers to focus on building features rather than managing infrastructure.
The Realtime service consists of three interconnected features that address different real-time scenarios. Broadcast enables direct client-to-client messaging with minimal latency, perfect for features like typing indicators or cursor tracking. Presence provides a mechanism for tracking which users are online and sharing their state across all connected clients. Postgres Changes listens to your database and pushes updates to clients whenever records are inserted, updated, or deleted. Together, these features form a comprehensive toolkit for building interactive applications.
Supabase Realtime is built on Phoenix Channels, an established WebSocket framework from the Elixir ecosystem known for handling millions of concurrent connections efficiently. This architectural foundation means the Realtime service can scale to meet the demands of high-traffic applications while maintaining low latency. The service is hosted across multiple regions globally, ensuring that users receive updates quickly regardless of their geographic location.
Three powerful features that enable real-time functionality
Broadcast
Low-latency client-to-client messaging for typing indicators, cursor tracking, and ephemeral events
Presence
Track and synchronize user state across clients, showing who's online and their current activity
Postgres Changes
Listen to database changes in real-time with INSERT, UPDATE, and DELETE event notifications
Core Architecture
The Realtime architecture centers around channels, which act as communication pipelines for groups of clients. A channel is identified by a name string that clients use to subscribe and receive messages. When a client connects and subscribes to a channel, it establishes a WebSocket connection to a Realtime server and becomes part of that channel's audience. All messages sent to the channel are then broadcast to every subscribed client.
Channels can be organized hierarchically using naming conventions that match your application structure. For example, a chat application might use channels like "room_123" for specific chat rooms, allowing messages to be delivered only to users in that room. The channel system supports both public channels accessible to any client and private channels that require authentication.
Under the hood, Supabase Realtime uses a multi-node architecture with servers distributed across regions. When you enable Realtime for your project, the system creates a Realtime cluster that connects to your PostgreSQL database through a logical replication slot. This connection allows the Realtime service to receive database changes as they occur, without impacting database performance through polling or frequent queries. The Phoenix Channels architecture handles millions of concurrent connections efficiently, making this infrastructure suitable for applications of any scale.
For a deeper dive into Supabase's PostgreSQL foundation, see our Supabase Database guide.
1import { createClient } from '@supabase/supabase-js'2 3const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)7 8// Subscribe to a channel9const channel = supabase10 .channel('room_123')11 .on('broadcast', { event: 'cursor' }, (payload) => {12 console.log('Received cursor:', payload)13 })14 .subscribe((status) => {15 console.log('Subscription status:', status)16 })Supabase vs Firebase Realtime
The backend-as-a-service market offers several options for real-time functionality, with Firebase Realtime Database and Firestore being the most prominent alternatives to Supabase. Understanding the differences helps developers make informed decisions about which platform best fits their requirements. Our recommendation of Supabase over Firebase stems from several technical and philosophical advantages that align with modern development practices.
Firebase Realtime Database stores data as a single JSON tree, which works well for simple hierarchical data but becomes cumbersome as your data model grows more complex. Queries are limited to filtering on child keys, and you cannot filter based on multiple conditions or perform joins between collections. This limitation often requires denormalizing data, which can lead to synchronization challenges and data consistency issues. Supabase Realtime, in contrast, leverages the full power of PostgreSQL, allowing you to use complex SQL queries to filter exactly the data you need updates for.
Firestore improves on the original Firebase Realtime Database by offering a document-based model with collections and subcollections, but it still operates as a NoSQL database with its associated trade-offs. Firestore charges based on document reads, writes, and storage, which can become expensive for applications with frequent real-time updates. Supabase provides a generous free tier for Realtime and uses PostgreSQL's efficient replication mechanism, resulting in more predictable costs as your application scales.
Perhaps most importantly, Supabase is built on open-source technology. The Realtime server is available on GitHub, and you can self-host Supabase if needed. This openness provides confidence that you're not locked into a proprietary platform and can migrate your application if requirements change. Firebase, while reliable, is a closed ecosystem where you're dependent on Google's roadmap and pricing decisions.
Subscriptions: The Foundation of Realtime
Subscriptions form the foundation of Supabase Realtime's functionality. A subscription represents a client's ongoing connection to receive updates through a channel. When you create a subscription, you're establishing a persistent WebSocket connection that remains open, allowing messages to flow bidirectionally between the client and the server. This persistent connection eliminates the need for repeated HTTP requests and enables the immediate delivery of updates.
The subscription process begins when a client calls the channel method on the Supabase client and specifies a channel name. The client then calls the subscribe method to establish the connection. The subscription moves through several states: SUBSCRIBING while the connection is being established, SUBSCRIBED when the server confirms the subscription is active, and CLOSED if the connection fails or is intentionally terminated. Your application can listen for these state changes to provide feedback to users and handle reconnection logic.
Each channel supports multiple concurrent subscriptions from different clients. The Realtime server maintains a list of all subscribers for each channel and routes messages appropriately. When a message is broadcast to a channel, the server sends it to every subscribed client simultaneously. This fan-out pattern is highly efficient and scales well because the server handles the distribution rather than requiring clients to poll for updates.
Subscriptions can be configured with various options that affect their behavior. You can specify which events to listen for, filter based on payload content, or limit the subscription to specific data patterns. These filters are evaluated on the server side, reducing unnecessary network traffic by ensuring clients only receive relevant updates. For Postgres Changes subscriptions, you can filter by schema, table, and operation type, receiving notifications only for the database changes that matter to your application.
Broadcast: Low-Latency Client Messaging
Broadcast enables direct messaging between clients through Realtime channels with minimal latency. Unlike Postgres Changes, which sends database updates, Broadcast carries arbitrary payloads that you define. This flexibility makes Broadcast ideal for features like typing indicators, cursor positions, game events, or any situation where you need to send data that doesn't originate from the database. The payloads are ephemeral--they're delivered immediately but not stored, making Broadcast efficient for transient communication.
The primary advantage of Broadcast over traditional API calls is latency. When a client sends a Broadcast message, it's transmitted directly through the WebSocket connection to the Realtime server, which immediately fans it out to all other subscribers of that channel. This end-to-end latency is typically under 100 milliseconds, compared to several hundred milliseconds for HTTP requests. For features like cursor tracking or typing indicators, this speed difference is immediately noticeable to users.
Broadcast messages are sent using the send method on a channel. The message is wrapped in an event that clients can listen for, and the payload can contain any JSON-serializable data. You have complete control over the event name and payload structure, allowing you to design message formats that match your application's needs. A chat application might use events like "new_message", "typing_start", and "typing_stop", while a collaborative editor might use events like "cursor_move" and "selection_change".
The broadcast feature supports both one-to-one and one-to-many messaging patterns. One-to-many is the default--every subscriber to a channel receives the message. For one-to-one messaging, you can include a target identifier in the payload and have clients filter messages that don't match their ID. More sophisticated routing can be implemented by creating multiple channels organized by room, user, or any other dimension that makes sense for your application.
1// Send a cursor position update2channel.send({3 type: 'broadcast',4 event: 'cursor',5 payload: {6 user_id: 'user-123',7 x: 456,8 y: 789,9 timestamp: new Date().toISOString()10 }11})12 13// Listen for cursor updates14channel15 .on('broadcast', { event: 'cursor' }, (payload) => {16 updateCursorPosition(payload.user_id, payload.x, payload.y)17 })18 .subscribe()Implementing Chat Features
Chat applications represent a classic use case for Broadcast, combining multiple Realtime features to create a rich messaging experience. A complete chat implementation typically uses Broadcast for sending messages and typing indicators, Presence for showing who's online, and Postgres Changes for persisting messages to the database. Each feature plays a specific role in creating the real-time chat experience users expect.
When a user sends a message, the application should broadcast it immediately for instant delivery to other users in the same chat room. The message appears in the sender's interface immediately, providing responsive feedback, while simultaneously being stored in PostgreSQL for persistence. This optimistic UI update makes the application feel fast while ensuring messages are safely stored. The Postgres Changes subscription then notifies all clients about the new message, updating their local cache and ensuring eventual consistency.
Typing indicators use Broadcast with short-lived payloads that don't require database storage. When a user begins typing, the application broadcasts a "typing_start" event with the user's ID. Other clients receive this event and update their UI to show that the user is typing. After a few seconds of inactivity, a "typing_stop" event is sent to clear the indicator. These ephemeral messages keep the chat interface lively without cluttering the database with temporary state.
1// Chat channel with Broadcast for messages and typing2const chatChannel = supabase.channel(`chat:${roomId}`)3 4// Listen for new messages5chatChannel6 .on('broadcast', { event: 'new_message' }, (payload) => {7 appendMessage(payload)8 })9 .on('broadcast', { event: 'typing_start' }, (payload) => {10 showTypingIndicator(payload.user_id)11 })12 .on('broadcast', { event: 'typing_stop' }, (payload) => {13 hideTypingIndicator(payload.user_id)14 })15 .subscribe()16 17// Send a message18function sendMessage(content: string) {19 const message = { user_id, content, timestamp }20 chatChannel.send({21 type: 'broadcast',22 event: 'new_message',23 payload: message24 })25 // Also persist to database26 await supabase.from('messages').insert(message)27}28 29// Typing indicators30let typingTimeout: NodeJS.Timeout31function onTyping() {32 chatChannel.send({33 type: 'broadcast',34 event: 'typing_start',35 payload: { user_id }36 })37 clearTimeout(typingTimeout)38 typingTimeout = setTimeout(() => {39 chatChannel.send({40 type: 'broadcast',41 event: 'typing_stop',42 payload: { user_id }43 })44 }, 2000)45}Presence: Tracking User State
Presence provides a mechanism for tracking and synchronizing shared state across multiple clients. Unlike Broadcast, which sends individual messages, Presence maintains a continuously synchronized view of all connected clients and their current state. This synchronization happens automatically through the Presence system, eliminating the need for complex state management code. Presence is ideal for showing who's online, tracking user status, or sharing ephemeral state that doesn't belong in the database.
The Presence system operates through three event types that correspond to changes in the shared state. The sync event fires when the complete presence state has been updated, providing a snapshot of all connected clients and their current payloads. The join event fires when a new client starts tracking presence, allowing you to react immediately to new participants. The leave event fires when a client disconnects or stops tracking presence, enabling you to clean up references to users who are no longer present.
Each client publishes a presence payload using the track method, which contains a small piece of state--typically user information or status. The Realtime server stores each client's payload under a unique key (often the user ID) and maintains a merged view of all connected clients. When any client updates their payload by calling track again, the change is propagated to all subscribers through sync events. This automatic merging ensures that all clients eventually converge on the same view of presence state.
The presence payload is limited in size but can contain any JSON-serializable data. Common payloads include user ID, display name, avatar URL, current activity, or status message. A collaborative application might include the user's cursor position in the presence payload, updating it as the user moves through the document. This approach keeps cursor positions synchronized along with presence state, simplifying the implementation of collaborative features.
For secure user authentication that integrates with Presence tracking, see our Supabase Authentication guide.
1const presenceChannel = supabase.channel('document-123')2 3// Listen to presence events4presenceChannel5 .on('presence', { event: 'sync' }, () => {6 const state = presenceChannel.presenceState()7 updateOnlineUsers(state)8 })9 .on('presence', { event: 'join' }, ({ key, newPresences }) => {10 console.log('User joined:', key, newPresences)11 })12 .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {13 console.log('User left:', key, leftPresences)14 })15 .subscribe(async (status) => {16 if (status === 'SUBSCRIBED') {17 // Track presence with user info18 await presenceChannel.track({19 user_id: currentUser.id,20 name: currentUser.name,21 avatar_url: currentUser.avatar,22 color: assignedColor,23 online_at: new Date().toISOString()24 })25 }26 })27 28// Untrack when leaving29presenceChannel.untrack()Postgres Changes: Database Event Subscriptions
Postgres Changes extends Supabase Realtime by listening to your PostgreSQL database and broadcasting changes to subscribed clients. This feature transforms PostgreSQL into an event source, allowing your application to respond immediately when data changes rather than polling for updates. Changes to any table with Realtime enabled are captured through PostgreSQL's logical replication mechanism and delivered to clients with minimal delay.
The Postgres Changes system captures three types of database events: INSERT when new rows are created, UPDATE when existing rows are modified, and DELETE when rows are removed. Each event includes the full new row data for INSERT and UPDATE events, and the primary key for DELETE events. This information allows clients to update their local state or UI accordingly, maintaining consistency with the database without manual refresh.
Subscribing to Postgres Changes requires configuring which schema, table, and events to listen for. In the Supabase dashboard, you can enable Realtime for individual tables, and optionally configure RLS policies that determine which users receive updates. By default, Realtime respects your Row Level Security policies, ensuring that users only receive updates for rows they are authorized to access. This integration with RLS means database security and real-time permissions are managed in the same place. For complete RLS configuration details, see our Supabase Row Level Security guide.
Filtering capabilities allow you to subscribe to subsets of changes rather than every modification to a table. You can filter by operation type (INSERT, UPDATE, or DELETE), by specific columns, or by the old and new values in UPDATE events. For example, a notification system might only subscribe to INSERT events on a notifications table, while a dashboard might subscribe to UPDATE events where the status column changes. Filters are specified when creating the subscription and evaluated on the server side.
1// Subscribe to notifications table changes2const notificationsChannel = supabase3 .channel('notifications')4 .on(5 'postgres_changes',6 {7 event: 'INSERT',8 schema: 'public',9 table: 'notifications',10 filter: 'user_id=eq.' + currentUser.id11 },12 (payload) => {13 showNotification(payload.new)14 }15 )16 .subscribe()17 18// Multiple event types19const todoChannel = supabase20 .channel('todos')21 .on('postgres_changes', {22 event: '*',23 schema: 'public',24 table: 'todos',25 filter: 'assigned_to=eq.' + currentUser.id26 }, (payload) => {27 switch (payload.eventType) {28 case 'INSERT':29 addTodo(payload.new)30 break31 case 'UPDATE':32 updateTodo(payload.new)33 break34 case 'DELETE':35 removeTodo(payload.old.id)36 break37 }38 })39 .subscribe()Performance Considerations
Performance is critical for real-time features, as latency directly impacts user experience. Supabase Realtime is designed for low-latency delivery, with WebSocket connections maintained to nearby servers and efficient message routing. Understanding the factors that affect latency helps you design implementations that perform well even at scale. Global distribution means users connect to regionally close Realtime servers, minimizing network round-trip time.
Message size affects both latency and bandwidth consumption. Larger payloads take longer to transmit and require more processing on both sender and receiver. Design your Broadcast payloads and Postgres Changes payloads to include only the information necessary for the use case. For large objects, send identifiers and load the full data separately rather than embedding the entire object in every message. This optimization keeps real-time messages fast and responsive.
Connection management becomes important as applications grow. Each connected client maintains a WebSocket connection that consumes server resources. Supabase's pricing tiers include connection limits, and aggressive connection management can help you stay within these limits. Implement proper cleanup by unsubscribing from channels when users navigate away, and detect and handle stale connections that haven't closed properly. These practices keep resource usage efficient.
Scaling strategies depend on your application's specific requirements. For applications with many independent channels, the Realtime service scales horizontally across multiple servers. For applications with a few very active channels, ensure your subscription filters are as specific as possible to reduce the fan-out of each message. Monitoring your application's real-time metrics helps identify bottlenecks and guides optimization efforts.
To integrate Supabase Realtime with a modern web framework, see our guide on Supabase with Next.js.
Security and Authorization
Security is fundamental to Supabase Realtime's design, with Row Level Security policies providing authorization for real-time subscriptions. This integration means your existing RLS policies that secure your database also control who receives real-time updates. A user who cannot SELECT rows from a table will not receive Postgres Changes notifications for those rows. This consistency simplifies security management and ensures that real-time features don't inadvertently expose unauthorized data.
Broadcast and Presence features support their own authorization mechanism independent of database RLS. When creating a channel, you can define which clients are authorized to subscribe and what events they can send or receive. Private channels require authentication before subscriptions are accepted, ensuring that only authorized users can participate. This authorization happens at the subscription level before any real-time traffic flows.
Implementing channel authorization requires creating a custom claim or token that verifies the caller's identity and permissions. When a client subscribes to a private channel, the Realtime server validates the authorization token. If the token is valid and authorizes access to the channel, the subscription proceeds. If not, the subscription is rejected with an appropriate error. This pattern keeps authorization logic centralized and secure.
Realtime authorization integrates with Supabase's authentication system, allowing you to use JWT claims to encode permissions. The user ID from the authentication token is available in authorization logic, enabling policies that grant access based on user identity, team membership, or any other claim present in the token. This flexibility supports sophisticated authorization scenarios while maintaining the simplicity of RLS-based security.
1-- Enable RLS for the table2ALTER TABLE messages ENABLE ROW LEVEL SECURITY;3 4-- Policy: Users only see messages in their conversations5CREATE POLICY "Users see messages in their conversations"6ON messages FOR SELECT7USING (8 EXISTS (9 SELECT 1 FROM conversation_members10 WHERE conversation_members.conversation_id = messages.conversation_id11 AND conversation_members.user_id = auth.uid()12 )13);14 15-- With Realtime enabled, users only receive notifications16-- for messages they're authorized to seeUse Cases and Implementation Patterns
The combination of Broadcast, Presence, and Postgres Changes enables a wide range of real-time applications. Understanding common patterns helps you apply these features effectively to your own projects. Each pattern emphasizes different aspects of the Realtime toolkit, demonstrating how the features work together to create compelling user experiences.
Live collaboration tools leverage all three Realtime features in concert. Presence tracks who is currently working in a document, showing avatars and online status. Broadcast sends cursor positions and selections at high frequency, creating the sensation of working together in real time. Postgres Changes persists edits to the database and notifies collaborators about changes they didn't make themselves. This combination creates a seamless collaborative editing experience.
Notification systems use Postgres Changes to detect new notifications and Broadcast to push them to connected clients. When a new notification is inserted, the database change triggers subscriptions, which push the notification data to the affected user. For users with multiple devices, Presence ensures notifications appear on all active connections. This pattern ensures that important information reaches users immediately regardless of which device they're using.
Multiplayer gaming uses Broadcast for low-latency game events like player movements and actions. The ephemeral nature of Broadcast messages is perfect for frequent, temporary updates that don't need persistence. Presence tracks the roster of players in a game session, showing who is online and their status. Game state persistence might use Postgres Changes or a separate database, depending on requirements for replay and history.
Collaborative Editing
Use Presence for user presence, Broadcast for cursor positions and selections, Postgres Changes for document persistence.
Live Dashboards
Postgres Changes for real-time metrics, Broadcast for alert notifications, automatic chart updates on data changes.
Multiplayer Games
Broadcast for low-latency game events, Presence for player roster, database for game state persistence.
Notification Systems
Postgres Changes for new notifications, Broadcast to push to connected clients, Presence for multi-device sync.
Team Collaboration
Online status with Presence, typing indicators with Broadcast, file updates with Postgres Changes.
Live Auctions
Real-time bid updates with Postgres Changes, presence tracking for active bidders, broadcast for bid confirmations.
Frequently Asked Questions
How does Supabase Realtime compare to Firebase Realtime Database?
Supabase uses PostgreSQL with SQL queries for complex filtering, offers true open-source technology, and integrates RLS policies for security. Firebase uses a proprietary NoSQL model with different query capabilities and pricing structure.
What is the latency for Supabase Realtime?
Typical end-to-end latency is under 100 milliseconds for Broadcast messages. Postgres Changes latency depends on database write timing and replication lag. Global server distribution minimizes network round-trip time.
How many concurrent connections does Supabase Realtime support?
Connection limits vary by pricing tier. The free tier includes 50 concurrent connections, Pro supports 200, and Enterprise handles higher limits. Phoenix Channels underlying architecture handles millions of connections.
Can I use Supabase Realtime with my existing PostgreSQL database?
Supabase Realtime works with Supabase-managed PostgreSQL instances. For self-hosted PostgreSQL, you would need to set up the Realtime server separately. The Realtime service requires logical replication access to the database.
How does offline support work with Supabase Realtime?
Realtime connections don't inherently support offline mode. Implement offline support by queuing changes locally, synchronizing when reconnected, and using Postgres Changes to receive updates that occurred while offline.
What happens if a message is lost?
Broadcast messages are ephemeral and not guaranteed to be delivered. For critical data, persist to the database and use Postgres Changes for delivery confirmation. Implement acknowledgment callbacks for important broadcasts.