Real-Time Apps with Supabase Realtime in Flutter
Learn how to build real-time Flutter apps with Supabase Realtime — database changes, presence, and broadcast across connected clients.
Published on • August 18, 2026
AI Assistant

A chat message, a live score, a shared cursor — anything that must appear on another user’s screen the instant it happens needs real-time sync. Polling is the easy but wrong answer; Supabase Realtime is the right one. Built on Postgres logical replication, it streams database changes to connected clients over WebSockets, and it also supports two additional primitives: presence (who’s online) and broadcast (client-to-client messages).
In this tutorial, you will learn how to use Supabase Realtime from a Flutter app: subscribe to database changes, track presence, and broadcast events. You’ll build a live-updating feed and a “who’s online” indicator. Key technologies: Supabase, Postgres, Flutter, the supabase_flutter SDK.
Prerequisites
- A Supabase project (free tier works)
- Flutter 3.x installed
- Basic familiarity with Postgres tables and RLS (Row Level Security)
Core Content
Set up the SDK
Add the dependency and initialize it in main():
dependencies:
supabase_flutter: ^2.0.0
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'https://your-project.supabase.co',
anonKey: 'your-anon-key',
);
runApp(const MyApp());
}
final supabase = Supabase.instance.client;
Stream database changes
Supabase Realtime listens to Postgres changes. First, a small table — say messages with id, content, created_at:
create table messages (
id bigint generated always as identity primary key,
content text not null,
created_at timestamptz not null default now()
);
Then subscribe from Flutter. StreamBuilder is a natural fit:
final stream = supabase
.from('messages')
.stream(primaryKey: ['id'])
.order('created_at');
StreamBuilder<List<Map<String, dynamic>>>(
stream: stream,
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final messages = snapshot.data!;
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) => ListTile(
title: Text(messages[index]['content'] as String),
),
);
},
);
The .stream() method keeps the list in sync with the database: inserts, updates, and deletes are pushed to every connected client. The primaryKey option lets Supabase correctly merge change events with local state.
Insert and see it live
When one client inserts a row, every subscribed client’s stream emits it automatically:
Future<void> sendMessage(String content) async {
await supabase.from('messages').insert({'content': content});
}
No polling, no refresh button — the row appears on all screens that subscribe to the messages table.
Track presence
Presence shows who’s online. Users join a channel, their state is tracked, and clients can react when the roster changes:
final channel = supabase.channel('room_1');
final presenceStream = channel.onPresenceSync((_) {
final states = channel.presenceState();
// Map of user id -> list of presence states
});
await channel.subscribe();
await channel.track({'user': userId, 'online_at': DateTime.now().toIso8601String()});
When a user disconnects (app closes, network drops), Supabase automatically removes their presence state after a timeout — no manual cleanup needed.
Broadcast between clients
For transient events — a typing indicator, a move in a shared game — broadcast is the tool. Messages go to everyone else on the channel without touching the database:
final channel = supabase.channel('game_1');
channel.onBroadcast(event: 'move', callback: (payload) {
print('Opponent moved: ${payload['x']}, ${payload['y']}');
});
await channel.subscribe();
await channel.sendBroadcastMessage('move', {'x': 10, 'y': 20});
Broadcast is fire-and-forget: if a client isn’t subscribed, it misses the message. Use it for ephemeral events, not durable state.
Secure real-time with RLS
Realtime respects Postgres Row Level Security. Without RLS, users can subscribe to anything. A basic policy limits reads to authenticated users:
alter table messages enable row level security;
create policy "Users can read messages"
on messages for select
to authenticated
using (true);
Realtime only delivers rows the user is permitted to select — so the same security model protects your live streams.
Putting It All Together
A complete real-time app uses all three primitives: .stream() on a table for durable data (messages, feeds, scores), presence on a channel for “who’s online”, and broadcast for ephemeral events (typing, moves). All of it rides one WebSocket connection, protected by RLS, and driven by Postgres logical replication — which means your server-side data is already the source of truth.
Conclusion & Next Steps
You’ve built real-time Flutter features with Supabase: live database streams, presence tracking, and client broadcast, all secured with RLS.
Next Steps: build a full chat with per-room channels, add optimistic updates with PostgrestTransformBuilder streaming, and explore Supabase Realtime’s self-hosted deployment for production-scale multi-node setups.
References:
- Supabase — Documentation. https://supabase.com/docs
- Supabase — Realtime guide. https://supabase.com/docs/guides/realtime
- Supabase — Flutter/Dart reference. https://supabase.com/docs/reference/dart/introduction
- Supabase — Row Level Security. https://supabase.com/docs/guides/database/postgres/row-level-security