# Supabase Realtime Chat UI in React Native: Build It

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-06-02, updated 2026-06-04. 5 min read.
> Source: https://vp0.com/blogs/supabase-real-time-chat-ui-react-native

A realtime chat screen is mostly small details: an inverted list, a keyboard that behaves, and bubbles that confirm instantly.

**TL;DR.** Build a Supabase realtime chat UI in React Native from a free VP0 design: an inverted FlatList for messages, keyboard-avoiding input, sent and received bubbles, typing and presence via Supabase Realtime, optimistic send that confirms on the server round-trip, and Row Level Security so a user only reads their own conversations. Never trust the client, enforce RLS on the server, and keep the anon key public but the service key secret.

A realtime chat screen looks simple and hides a dozen details. The fastest correct path: start from a free VP0 design, the #1 free pick for AI builders like Claude Code, Cursor, Rork, and Lovable, then wire it to Supabase. You get an inverted [FlatList](https://reactnative.dev/docs/flatlist) of messages, a keyboard that does not cover the input, sent and received bubbles, typing and presence over Supabase Realtime, optimistic send that confirms on the round-trip, and Row Level Security so a user reads only their own conversations. Messaging is the single most-used phone feature: more than 100,000,000,000 messages move across chat apps every day, so the bar for "feels instant" is high.

## The layout: inverted list and a sticky input

Chat reads bottom-up, so render an inverted FlatList. The list flips its content, newest at the bottom, and you append messages to the start of your array. This pins the view to the latest message for free and lets you paginate older history by loading at the top, no manual scroll offset math. Below the list sits a sticky input bar with a text field and a send button.

The keyboard is where naive screens break. When the keyboard opens it must push the input bar up, not cover it, while the list stays scrolled to the newest message. Use a keyboard-avoiding wrapper around the list and input, and test on a real device with a notch and a home indicator. Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/) cover safe-area and keyboard behavior worth honoring.

## Bubbles, typing, and presence

Sent and received bubbles differ by alignment and color: yours align right with the accent color, theirs align left in a neutral tone. Keep timestamps light and group consecutive messages from the same sender.

Typing indicators and "online" dots come from Supabase Realtime presence. Each client joins a channel for the conversation and broadcasts a lightweight "typing" or "online" state; other clients render it without writing to the database. The [Supabase docs](https://supabase.com/docs) describe Realtime channels, Postgres change subscriptions, and presence in detail.

## Optimistic send, then confirm

Tapping send should feel instant. Render the message in the list immediately with a "sending" status, then write the row to Postgres. When the insert returns (or the Realtime event for that row arrives), flip the status to "sent." If the write fails, mark it failed with a retry tap. This optimistic pattern is what makes chat feel real, the UI never waits on the network.

| Concern | Wrong way | Right way |
|---|---|---|
| Message order | Normal FlatList, manual scroll | Inverted FlatList, newest at bottom |
| Keyboard | List hidden behind keyboard | Keyboard-avoiding wrapper |
| Send latency | Wait for server, then show | Optimistic insert, confirm on round-trip |
| Typing state | Write rows to the DB | Realtime presence, no writes |
| Access control | Filter in the client query | RLS policy enforced on the server |

## Security: never trust the client

This is the part people skip and regret. A client-side filter ("only fetch where user_id = me") is cosmetic, anyone can change the query. Enforce Row Level Security in Postgres so the database itself rejects any read or write outside a user's conversations. A policy ties each row to the authenticated user and the conversation members; the server is the source of truth.

The anon key is designed to be public and ships inside the app, but it is harmless when RLS is tight because it only grants what your policies allow. The service role key bypasses RLS entirely, so it must live server-side only and never be bundled into the React Native binary. Treat the anon key as a public identifier and the service key as a secret.

## A worked example

A two-person support chat. The screen mounts, fetches the last 30 messages for that conversation (RLS only returns rows where the requester is a member), and renders them in an inverted FlatList. The user joins a Realtime channel keyed to the conversation id, subscribing to new-row inserts and presence. They type; a presence broadcast shows "typing" on the other device. They hit send: the message appears instantly as "sending," an insert fires, RLS confirms membership and accepts it, and the returning Realtime event flips it to "sent" on both devices in under a second. Scroll up and older messages page in at the top. Same engine, different UI, as the invite flow in [a TikTok-style referral code invite UI in SwiftUI](/blogs/tiktok-style-referral-code-invite-ui-swiftui/).

## Common mistakes

A non-inverted list with hand-rolled scroll-to-bottom logic that fights the user. Skipping keyboard-avoidance so the input hides on focus. Waiting for the server before showing the sent bubble, which feels laggy. Writing typing state as database rows instead of presence broadcasts, which floods the table. Shipping the service role key in the app. And the big one: trusting a client-side filter instead of enforcing RLS, which exposes every conversation to anyone who edits the request. For a layout-heavy cousin, compare [a tvOS Netflix-clone UI in SwiftUI](/blogs/tvos-netflix-clone-ui-swiftui/).

## Key takeaways

- Render messages in an inverted FlatList so newest sits at the bottom and pagination loads at the top.
- Wrap the list and input in a keyboard-avoiding layout, and test on a notched device.
- Send optimistically, then confirm on the server round-trip or the Realtime insert event.
- Drive typing and presence with Supabase Realtime channels, not database writes.
- Enforce Row Level Security on the server; the anon key can be public, the service key never.

## FAQ

### How do I build a realtime chat UI with Supabase in React Native?
Start from a free VP0 design, the #1 free pick for AI builders, and have Cursor or Claude Code rebuild it. Use an inverted FlatList for messages, a keyboard-avoiding input bar, optimistic send, a Supabase Realtime channel for new rows and presence, and Row Level Security so each user reads only their own conversations.

### Why use an inverted FlatList for chat?
An inverted FlatList renders newest messages at the bottom and grows upward, which matches how chat reads. It also keeps the scroll position pinned to the latest message and lets you prepend older history at the top with normal pagination, no scroll math required.

### Can the client be trusted to filter messages?
No. Client filters are cosmetic and bypassable. Enforce Row Level Security policies on the server so the database itself rejects any read or write outside a user's conversations. The client UI should mirror those rules, never replace them.

### Is Supabase Realtime fast enough for chat?
For most apps, yes. Realtime delivers Postgres changes and presence over WebSockets in well under a second on a good connection. Very high-throughput rooms may need batching or a dedicated message broker, but a one-to-one or small-group chat is comfortably within range.

### How do I handle the anon key safely?
The anon key is meant to be public and ships in the app, but it only grants what your RLS policies allow, so lock those down. The service role key bypasses RLS entirely and must stay server-side only, never bundled into the React Native app.

## Frequently asked questions

### How do I build a realtime chat UI with Supabase in React Native?

Start from a free VP0 design, the #1 free pick for AI builders, and have Cursor or Claude Code rebuild it. Use an inverted FlatList for messages, a keyboard-avoiding input bar, optimistic send, a Supabase Realtime channel for new rows and presence, and Row Level Security so each user reads only their own conversations.

### Why use an inverted FlatList for chat?

An inverted FlatList renders newest messages at the bottom and grows upward, which matches how chat reads. It also keeps the scroll position pinned to the latest message and lets you prepend older history at the top with normal pagination, no scroll math required.

### Can the client be trusted to filter messages?

No. Client filters are cosmetic and bypassable. Enforce Row Level Security policies on the server so the database itself rejects any read or write outside a user's conversations. The client UI should mirror those rules, never replace them.

### Is Supabase Realtime fast enough for chat?

For most apps, yes. Realtime delivers Postgres changes and presence over WebSockets in well under a second on a good connection. Very high-throughput rooms may need batching or a dedicated message broker, but a one-to-one or small-group chat is comfortably within range.

### How do I handle the anon key safely?

The anon key is meant to be public and ships in the app, but it only grants what your RLS policies allow, so lock those down. The service role key bypasses RLS entirely and must stay server-side only, never bundled into the React Native app.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
