Supabase Realtime lets your app receive database changes as they happen, without polling. Build live feeds, chat apps, collaborative tools, or any feature that needs instant updates.

Assumes Supabase is initialized. See Auth for setup.

Stream with StreamBuilder

The simplest way to get real-time data. The stream() method returns a Dart Stream that emits the full current dataset every time any row changes. Plug it into a StreamBuilder and your UI updates automatically.

The primaryKey parameter tells Supabase which column uniquely identifies each row, so it can track changes correctly.

dart
StreamBuilder(
  stream: supabase
      .from('messages')
      .stream(primaryKey: ['id'])
      .order('created_at'),
  builder: (context, snapshot) {
    if (!snapshot.hasData) {
      return Center(child: CircularProgressIndicator());
    }

    final messages = snapshot.data!;
    return ListView.builder(
      itemCount: messages.length,
      itemBuilder: (context, index) {
        final msg = messages[index];
        return ListTile(
          title: Text(msg['content']),
          subtitle: Text(msg['user_name']),
        );
      },
    );
  },
)

stream() handles reconnection automatically and returns the full list each time. For most use cases (chat, live lists), this is all you need.

Postgres Changes

For more control, subscribe to a channel and listen for specific database events. You get the exact row that changed, plus the old values for updates and deletes. This is useful when you want to update a local list incrementally instead of getting the full dataset each time.

dart
final channel = supabase.channel('public:messages');

channel
    .onPostgresChanges(
      event: PostgresChangeEvent.all,
      schema: 'public',
      table: 'messages',
      callback: (payload) {
        print('Event: ${payload.eventType}');  // INSERT, UPDATE, or DELETE
        print('New row: ${payload.newRecord}');
        print('Old row: ${payload.oldRecord}');
      },
    )
    .subscribe();

Filter by Event

You can listen to only inserts, only updates, or only deletes by changing the event type.

dart
// Only new rows
channel.onPostgresChanges(
  event: PostgresChangeEvent.insert,
  schema: 'public',
  table: 'messages',
  callback: (payload) {
    final newMessage = payload.newRecord;
    // Add to your local list
  },
);

// Only updates
channel.onPostgresChanges(
  event: PostgresChangeEvent.update,
  schema: 'public',
  table: 'messages',
  callback: (payload) {
    final updated = payload.newRecord;
    // Update in your local list
  },
);

// Only deletes
channel.onPostgresChanges(
  event: PostgresChangeEvent.delete,
  schema: 'public',
  table: 'messages',
  callback: (payload) {
    final deleted = payload.oldRecord;
    // Remove from your local list
  },
);

Always clean up channels when the widget is removed to avoid memory leaks:

dart
@override
void dispose() {
  supabase.removeChannel(channel);
  super.dispose();
}

Presence

Presence tracks which users are currently online. Each connected client broadcasts a small state object (like their user ID and online timestamp), and you get notified when anyone joins or leaves. Useful for showing online indicators, active user counts, or who is viewing a document.

dart
final channel = supabase.channel('room:lobby');

channel
    .onPresenceSync((payload) {
      // Called whenever the presence state changes
      final state = channel.presenceState();
      print('Currently online: ${state.length} users');
    })
    .onPresenceJoin((payload) {
      print('User joined: ${payload.newPresences}');
    })
    .onPresenceLeave((payload) {
      print('User left: ${payload.leftPresences}');
    })
    .subscribe((status, error) async {
      if (status == RealtimeSubscribeStatus.subscribed) {
        // Announce this user's presence
        await channel.track({
          'user_id': supabase.auth.currentUser!.id,
          'online_at': DateTime.now().toIso8601String(),
        });
      }
    });

Broadcast

Broadcast sends messages between connected clients without storing anything in the database. The data is ephemeral (it disappears when the connection closes). Use it for typing indicators, cursor positions, live reactions, or any short-lived communication.

dart
final channel = supabase.channel('room:chat');

// Listen for broadcast messages
channel
    .onBroadcast(
      event: 'typing',
      callback: (payload) {
        print('${payload['user_name']} is typing...');
      },
    )
    .subscribe();

// Send a broadcast message
await channel.sendBroadcastMessage(
  event: 'typing',
  payload: {'user_name': 'Mitch'},
);

Full Widget Pattern

A complete example showing how to set up a realtime channel in a stateful widget and clean it up properly when the widget is removed.

dart
class LiveMessages extends StatefulWidget {
  const LiveMessages({super.key});

  @override
  State<LiveMessages> createState() => _LiveMessagesState();
}

class _LiveMessagesState extends State<LiveMessages> {
  final List<Map<String, dynamic>> messages = [];
  late final RealtimeChannel channel;

  @override
  void initState() {
    super.initState();
    _loadInitial();
    _subscribeToChanges();
  }

  Future<void> _loadInitial() async {
    final data = await supabase
        .from('messages')
        .select()
        .order('created_at');
    setState(() => messages.addAll(List<Map<String, dynamic>>.from(data)));
  }

  void _subscribeToChanges() {
    channel = supabase.channel('public:messages');
    channel
        .onPostgresChanges(
          event: PostgresChangeEvent.insert,
          schema: 'public',
          table: 'messages',
          callback: (payload) {
            setState(() => messages.add(payload.newRecord));
          },
        )
        .subscribe();
  }

  @override
  void dispose() {
    supabase.removeChannel(channel);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: messages.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(messages[index]['content']));
      },
    );
  }
}