Skip to content
Blog

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: