DEV Community

kanta13jp1
kanta13jp1

Posted on

Build a Realtime Chat with Supabase: Presence, Broadcast, and DB Changes

Build a Realtime Chat with Supabase: Presence, Broadcast, and DB Changes

Build production-quality realtime chat without writing a single WebSocket line. Three Supabase Realtime channel types explained.

The Three Channel Types

1. Postgres Changes: receive INSERT/UPDATE/DELETE from the DB in real time
2. Broadcast:        send arbitrary messages directly between clients
3. Presence:         sync online user state across clients
Enter fullscreen mode Exit fullscreen mode

1. Postgres Changes — Receive New Messages

final subscription = supabase
    .channel('messages:room_${roomId}')
    .onPostgresChanges(
      event: PostgresChangeEvent.insert,
      schema: 'public',
      table: 'messages',
      filter: PostgresChangeFilter(
        type: PostgresChangeFilterType.eq,
        column: 'room_id',
        value: roomId,
      ),
      callback: (payload) {
        final newMessage = Message.fromJson(payload.newRecord);
        setState(() => _messages.add(newMessage));
      },
    )
    .subscribe();

@override
void dispose() {
  supabase.removeChannel(subscription);
  super.dispose();
}
Enter fullscreen mode Exit fullscreen mode

Schema + RLS:

CREATE TABLE messages (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id    UUID NOT NULL REFERENCES rooms(id),
  user_id    UUID NOT NULL REFERENCES auth.users(id),
  content    TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "room members can read messages"
  ON messages FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM room_members
      WHERE room_id = messages.room_id AND user_id = auth.uid()
    )
  );

ALTER PUBLICATION supabase_realtime ADD TABLE messages;
Enter fullscreen mode Exit fullscreen mode

2. Broadcast — Typing Indicators Without DB Writes

final channel = supabase.channel('room:$roomId');

// Send
channel.sendBroadcastMessage(
  event: 'typing',
  payload: {'user_id': userId, 'is_typing': true},
);

// Receive
channel.onBroadcast(
  event: 'typing',
  callback: (payload) {
    setState(() => _typingUsers[payload['user_id']] = payload['is_typing']);
  },
).subscribe();
Enter fullscreen mode Exit fullscreen mode

3. Presence — Who's Online

final channel = supabase.channel('room:$roomId');

channel.onPresenceSync(callback: (_) {
  final onlineUsers = channel.presenceState().entries
      .map((e) => e.value.first['user_id'] as String)
      .toList();
  setState(() => _onlineUsers = onlineUsers);
});

await channel.subscribe();
await channel.track({'user_id': userId, 'status': 'online'});

@override
void dispose() async {
  await channel.untrack();
  await supabase.removeChannel(channel);
  super.dispose();
}
Enter fullscreen mode Exit fullscreen mode

Complete Chat Widget

class _ChatPageState extends ConsumerState<ChatPage> {
  final _controller = TextEditingController();
  final List<Message> _messages = [];
  RealtimeChannel? _channel;

  @override
  void initState() {
    super.initState();
    _loadInitialMessages();
    _setupRealtime();
  }

  Future<void> _loadInitialMessages() async {
    final data = await supabase
        .from('messages')
        .select('*, profiles(username, avatar_url)')
        .eq('room_id', widget.roomId)
        .order('created_at')
        .limit(50);
    setState(() => _messages.addAll(data.map(Message.fromJson)));
  }

  void _setupRealtime() {
    _channel = supabase
        .channel('messages:${widget.roomId}')
        .onPostgresChanges(
          event: PostgresChangeEvent.insert,
          schema: 'public',
          table: 'messages',
          filter: PostgresChangeFilter(
            type: PostgresChangeFilterType.eq,
            column: 'room_id',
            value: widget.roomId,
          ),
          callback: (p) => setState(() => _messages.add(Message.fromJson(p.newRecord))),
        )
        .subscribe();
  }

  Future<void> _sendMessage() async {
    final text = _controller.text.trim();
    if (text.isEmpty) return;
    await supabase.from('messages').insert({'room_id': widget.roomId, 'content': text});
    _controller.clear();
  }

  @override
  void dispose() {
    _channel?.unsubscribe();
    _controller.dispose();
    super.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Real-time DB updates        → Postgres Changes
Direct client communication → Broadcast (typing indicators, reactions)
Online status               → Presence
Data protection             → RLS + supabase_realtime publication
Enter fullscreen mode Exit fullscreen mode

Supabase Realtime abstracts WebSocket complexity entirely. Focus on what matters: UI, schema design, and RLS policies.

Top comments (0)