Back to Blog
·12 min
BI

BreafIO Team

Product & Engineering

Real-Time React Native Chat App Tutorial with Supabase

Introduction

"Building real-time messaging used to require complex WebSocket cluster management. With Supabase Realtime and PostgreSQL, listening to database changes takes just a few lines of code."

Mobile applications demand fast, reliable message delivery. Traditional REST polling creates unnecessary server load and battery drain, while managing dedicated Socket.io servers adds operational overhead.

This guide demonstrates how to build a cross-platform real-time messaging application in React Native (Expo) powered by Supabase Realtime database channels and PostgreSQL Row Level Security (RLS).

1. Database Schema & Row Level Security (RLS)

First, define the core messages schema in PostgreSQL with RLS enabled to restrict channel access to authenticated users:

-- Create messages table
CREATE TABLE messages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id UUID NOT NULL,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  content TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Enable Row Level Security (RLS)
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- Allow authenticated users to view messages in their rooms
CREATE POLICY "Users can view room messages"
ON messages FOR SELECT
TO authenticated
USING (auth.uid() IS NOT NULL);

-- Allow authenticated users to send messages
CREATE POLICY "Users can insert messages"
ON messages FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

2. React Native Integration (Supabase Realtime Channel)

Using @supabase/supabase-js, subscribe directly to PostgreSQL INSERT changes on the messages table filtered by room_id:

import React, { useEffect, useState } from 'react';
import { View, FlatList, Text, TextInput, Button } from 'react-native';
import { supabase } from '../lib/supabaseClient';

export default function ChatRoom({ roomId, userId }: { roomId: string; userId: string }) {
  const [messages, setMessages] = useState<any[]>([]);
  const [text, setText] = useState('');

  useEffect(() => {
    // 1. Fetch historical messages
    fetchInitialMessages();

    // 2. Subscribe to real-time inserts via Supabase WebSocket channel
    const channel = supabase
      .channel(`room:${roomId}`)
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` },
        (payload) => {
          setMessages((prev) => [payload.new, ...prev]);
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [roomId]);

  const fetchInitialMessages = async () => {
    const { data } = await supabase
      .from('messages')
      .select('*')
      .eq('room_id', roomId)
      .order('created_at', { ascending: false });

    if (data) setMessages(data);
  };

  const sendMessage = async () => {
    if (!text.trim()) return;
    await supabase.from('messages').insert([{ room_id: roomId, user_id: userId, content: text }]);
    setText('');
  };

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <FlatList
        inverted
        data={messages}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => <Text>{item.content}</Text>}
      />
      <TextInput value={text} onChangeText={setText} placeholder="Type a message..." />
      <Button title="Send" onPress={sendMessage} />
    </View>
  );
}

3. Adding Rooms & Direct Messages

Extend the schema with a rooms table and a room_members junction table for multi-room support. For direct messages, create a unique constraint on (room_id, user_id) pairs where room_type = 'dm'.

4. Production Considerations for Mobile Realtime

Connection Reconnection: Handle background/foreground mobile state transitions (AppState API) to reconnect WebSockets automatically when the app returns from background.

Pagination: Implement cursor-based pagination using created_at timestamps to avoid loading thousands of messages at once into memory.

Optimistic UI Updates: Append messages locally to the list state immediately upon clicking "Send" for zero perceived UI latency, then reconcile upon server response.

Message Read Status: Track read receipts with a last_read_at column on room_members. Update it when the user opens a room.

Push Notifications: Use Expo Push Notifications with Supabase Edge Functions to send push alerts when a message arrives in a room the user is not currently viewing.

Conclusion

Supabase Realtime eliminates the infrastructure complexity of building real-time chat. PostgreSQL RLS provides security at the database level, the WebSocket channel API handles subscription management, and React Native's Expo framework handles cross-platform rendering. The result is a production-grade chat application with minimal backend code. Start with the schema, add the realtime subscription, and build out rooms and notifications as your user base grows.

Ready to Build?

Get started with our production-ready starter kits and ship your project faster.

Browse Starter Kits