Skip to content
Flutter+Supabase Realtime
Stack Integration

Flutter + Supabase Realtime: Live Data Sync

Supabase Realtime pushes Postgres row changes to Flutter clients over WebSockets — the pattern behind live order dashboards, collaborative features, and chat applications.

Use Cases
  1. Live order status updates in a Flutter restaurant or logistics app
  2. Real-time chat messages synced across all connected Flutter clients
  3. Collaborative editing with multiple users seeing changes simultaneously
  4. Live leaderboards and score updates in gaming apps
Implementation

Subscribe to a Supabase Realtime channel using supabase.channel('channel-name').onPostgresChanges(...). Call .subscribe() to activate and store the channel reference. Unsubscribe in your widget's dispose() method — supabase.removeChannel(channel) — to prevent memory leaks. RLS policies apply to Realtime subscriptions: a user only receives change events for rows their RLS policy permits them to read. Test subscription behaviour with explicit RLS policies in place before shipping.

In detail

How the pieces connect

Supabase Realtime runs as a server-side process that tails the Postgres write-ahead log (WAL) through logical replication. When a row changes on a table you've enabled for replication, the change is fanned out over a Phoenix-channels WebSocket to every subscribed client. On the Flutter side, supabase_flutter holds one shared RealtimeClient connection; each supabase.channel('name') you create multiplexes over that single socket rather than opening a new connection per subscription. You attach a listener with .onPostgresChanges(event: PostgresChangeEvent.insert, schema: 'public', table: 'orders', callback: ...), then .subscribe(). The callback fires with a PostgresChangePayload carrying newRecord and oldRecord maps. Crucially, you must enable the table for the supabase_realtime publication first (the dashboard's Replication settings, or ALTER PUBLICATION) — without that, no events are emitted no matter how the client subscribes.

Production gotchas

The most common failure is a silent subscription: the channel connects but no events arrive. Check three things in order — the table is in the supabase_realtime publication, the RLS policy on that table actually permits the current user to read the changed row, and REPLICA IDENTITY is set appropriately (default REPLICA IDENTITY omits old values, so oldRecord on updates/deletes can be empty unless you set REPLICA IDENTITY FULL). Second, the socket carries the user's auth context: after a token refresh or sign-in change, call supabase.realtime.setAuth() so Realtime re-evaluates RLS against the new JWT, otherwise events keep flowing under the stale identity until reconnect. Third, lifecycle. A channel left subscribed when a widget is rebuilt leaks; always removeChannel in dispose() and avoid recreating the channel on every build(). On mobile, the OS suspends the socket in the background — handle reconnect rather than assuming the stream is continuous.

Realtime is push, not the source of truth

A subtle correctness trap: Realtime delivers changes that happen after you subscribe. It is not a query. If you subscribe and then read, or read and then subscribe, you can miss or double-count rows in the gap between the two. The safe pattern is to subscribe first, run your initial select() to seed local state, and de-duplicate by primary key as live events arrive — treating each payload as an upsert into your in-memory list. Don't rely on event ordering across tables either; each change is independent. For features where exact consistency matters (counts, balances), use Realtime to trigger a re-fetch or rely on Postgres as the authority, rather than reconstructing state purely from the event stream. Realtime is excellent for liveness; Postgres queries remain the source of truth.

When this combo fits — and when it doesn't

Flutter plus Supabase Realtime fits when clients need to react to shared, persisted state: order boards, chat threads, presence, live dashboards. Postgres Changes is the right tool when the data already lives in a table and you want row-level pushes scoped by RLS. It's a poorer fit for very high-frequency ephemeral signals — cursor positions, typing indicators, fast-moving game state — because every change writes to Postgres and traverses the WAL. For those, use Realtime's Broadcast channels (client-to-client messages that never touch the database) or Presence for online-state tracking, both available on the same channel API. If your app only needs occasional updates, plain polling or a manual pull-to-refresh is simpler and cheaper than holding an open socket. Match the transport to the cadence.

Live ExamplePizzeria Bestek — Case Study
Other integration guidesView all →
Related

Need this built?