Flutter Supabase Chat Core
Add chat support to your app in 10 minutes!
A Supabase backend for flutter_chat_core and flutter_chat_ui 2.x: rooms, members, messages, attachments, read receipts, reactions, typing indicators and online status, over Postgres + RLS and Supabase Realtime.
ℹ️ Coming from 1.x (flutter_chat_types)? Read MIGRATION.md: 2.0 changes both the Dart API and the database schema. ℹ️
Actively maintained, community-driven Supabase BaaS for chat applications with an optional chat UI.
⚠️⚠️ This project has been optimized to support scalability, but careful evaluation is recommended before integrating it into large-scale projects, considering specific requirements and available resources. ⚠️⚠️
The example app on Supabase cloud: real-time messages and typing indicator, reactions, replies and voice messages.
Have you ever wanted to implement a chat in your application? Do you think it's difficult and complex?
Try this, thanks to the magnificent Supabase platform and the Flutter Chat UI package, you can achieve it in just a few minutes and effortlessly.
Flyer Chat is a platform for creating in-app chat experiences using Flutter. This repository contains Supabase BaaS implementation for Flutter. We are also working on our more advanced SaaS and self-hosted solutions.
-
Free, open-source and community-driven. We offer no paid plugins and strive to create an easy-to-use, almost drop-in chat experience for any application. Contributions are more than welcome! Please read our Contributing Guide.
-
Chat UI agnostic. You can choose the chat UI you prefer. But if you don't have one, we provide our own free and open-source Flutter Chat UI, which can be used to create a working chat in minutes.
-
Secure by default. Every table, storage bucket and Realtime channel is protected by Postgres Row Level Security. Check our documentation for the details.
Features
- Direct and group rooms, created atomically by a database function. Direct rooms are de-duplicated per pair of users (and per metadata scope, see Scoping).
- Group management: per-room roles (
admin,moderator), add/remove members, change roles, rename, leave, delete. A room left without members is deleted automatically. - Messages of every
flutter_chat_coretype (text, image, file, audio with waveform, video, custom...), with replies, edits, reactions, pinned messages and search. - Forwarding: forward messages to another room, keeping the original author and date; attachments are copied into the destination room.
- Optimistic sending: messages appear right away, are reconciled with the stored row and can be retried when they fail.
- Read receipts and unread counters:
markRoomAsReadflips other members' messages toseenand resets the room'sunreadCount. - Per-user preferences: pin a room on top of your list, mute it for a while or for ever.
- Realtime everywhere: message changes over private Broadcast channels, a throttled typing indicator, the rooms list kept in sync, online status over Presence.
- Cursor-based pagination for messages and offset pagination for rooms and users.
- Attachments and avatars in private Storage buckets, plus an Edge Function that deletes the files of deleted messages.
- Scoping hooks to keep the chats of different workspaces/organizations apart without forking the package.
- Typed errors: every failure surfaces as a
SupabaseChatCoreException. - Scales with history:
chats.messagesis partitioned by week withpg_partman, ids are time-ordered UUIDv7.
Requirements
Dart >=3.8.0andFlutter >=3.32.0.- A Supabase project (cloud or self-hosted) with:
- the
pg_partmanandpg_cronextensions available (the SQL scripts create them); - Realtime with Broadcast from Database support (Supabase cloud, or self-hosted
supabase/realtime>= v2.33): message changes are sent by a database trigger; - the
chatsschema exposed through the Data API (see below).
- the
Installation
dependencies:
flutter_supabase_chat_core: ^2.0.0-rc.2
supabase_flutter: ^2.17.2
uuid: ^4.5.1
# Optional, for the ready-made UI:
flutter_chat_ui: ^2.11.1
flyer_chat_text_message: ^2.6.0
flyer_chat_image_message: ^2.3.0
flyer_chat_file_message: ^2.4.0
flutter_chat_ui 2.x ships no renderer for images or files (only a minimal text fallback): add one flyer_chat_*_message package per message type you use. The example app pins flutter_chat_ui: '>=2.11.1 <2.12.0': 2.12.0 moved the Composer to the experimental material_ui package, which crashes inside a standard Scaffold ("No Material widget found").
Supabase setup
The database is set up by the SQL scripts in example/utils/sql, applied in this order:
| Script | Content |
|---|---|
01_database_schema.sql |
Schema chats, tables, weekly partitions, indexes, pg_cron maintenance job. Also migrates a 1.x database (see MIGRATION.md). |
02_database_trigger.sql |
Profile creation on sign-up, last message snapshot, sent status, media cleanup queue, empty room deletion, Realtime broadcast of message changes. |
03_database_policy.sql |
Access helpers, RPCs and RLS policies (tables and realtime.messages). |
04_storage.sql |
Buckets chats_assets and chats_user_avatar and their policies. |
05_database_view.sql |
Views chats.rooms_l and chats.messages_l. |
99_database_schema_permission.sql |
Final grants. |
06_cleanup_media_schedule.sql |
Optional: schedules the cleanup-media Edge Function (see Media cleanup). |
Every script is idempotent: re-running them is safe.
Option A: create a project with the Supabase CLI
- Install the Supabase CLI and log in with
supabase login. - Linux/macOS only: install
jq. - Run the script from
example/utils:
cd example/utils
./supabase_prepare.sh # Linux/macOS
cd example\utils
.\supabase_prepare.ps1 # Windows
It lets you create a new project (organization and region picked from a list, database password generated for you) or pick an existing one (it then asks for its database password), fetches the API keys and writes:
example/lib/supabase_options_for_test.dartandexample/lib/main_test.dart(gitignored), so you can run the example withflutter run -t lib/main_test.dart;example/utils/project-secrets.local.jsonwith the project ref, URL, keys and database credentials. It contains secrets: it is gitignored, never commit it.
Then it offers to apply scripts 01…05 and 99 through supabase link + supabase db query --linked (no direct psql connection needed).
Manual project creation
supabase projects create demo-chat(choose your organization, region, and a secure database password — save it, it won't be shown again)supabase projects listto get the project'sREFERENCE IDsupabase projects api-keys --project-ref <reference_id>to get theanon/publishable key
Option B: apply the scripts with psql
prepare.sh/prepare.ps1 apply scripts 01…05 and 99 with psql. They ask for the database password once (or read PGPASSWORD) and stop at the first failing script.
cd example/utils
./prepare.sh -h <host> -p <port> -d <database> -U <user>
cd example\utils
.\prepare.ps1 -hostname <host> -port <port> -database <database> -user <user>
The connection parameters are in the dashboard of your project:

Expose the chats schema
Add chats to the exposed schemas (Dashboard → Project Settings → Data API):

Optional, for testing only: disable email confirmation to sign up in one click.

Quick start
Initialize Supabase, then wrap your home page in UserOnlineStateObserver to publish the user's online status:
import 'package:flutter/material.dart';
import 'package:flutter_supabase_chat_core/flutter_supabase_chat_core.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'https://<project-ref>.supabase.co',
publishableKey: '<anon-or-publishable-key>',
);
runApp(
const MaterialApp(home: UserOnlineStateObserver(child: RoomsPage())),
);
}
SupabaseChatCore.instance is bound to Supabase.instance.client, so Supabase.initialize must complete before it is used. Users sign up with Supabase Auth: the chats.handle_new_user trigger creates their chat profile, reading first_name/last_name/avatar_url from the sign-up metadata when present.
Rooms list
SupabaseChatRoomsController is a ChangeNotifier holding a paginated, searchable list of the user's rooms, kept in sync over Realtime (pinned rooms first, then most recent activity):
class RoomsPage extends StatefulWidget {
const RoomsPage({super.key});
@override
State<RoomsPage> createState() => _RoomsPageState();
}
class _RoomsPageState extends State<RoomsPage> {
final _rooms = SupabaseChatRoomsController()..loadFirstPage();
@override
void dispose() {
_rooms.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Chats')),
body: ListenableBuilder(
listenable: _rooms,
builder: (context, _) => ListView.builder(
itemCount: _rooms.rooms.length,
itemBuilder: (context, index) {
final room = _rooms.rooms[index];
return ListTile(
title: Text(room.name ?? 'Chat'),
trailing: room.unreadCount > 0
? Badge(label: Text('${room.unreadCount}'))
: null,
onTap: () {
_rooms.markLocallyAsRead(room.id);
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => RoomPage(room: room)),
);
},
);
},
),
),
);
}
Call loadNextPage() when the list is scrolled near its end (e.g. from a ScrollController listener). It also exposes search(filter), refresh(), hasMore, isLoading, error, totalUnreadCount (muted rooms excluded) and pinRoom/muteRoom/unmuteRoom, which update the list right away.
Start conversations with SupabaseChatCore.instance.createRoom(otherUser) (direct) or createGroupRoom(name: ..., users: [...]); pick users with users(filter: ..., excludeLoggedUser: true).
Conversation
SupabaseChatController implements flutter_chat_core's ChatController: create one per open room and pass it to Chat.
import 'package:flutter_chat_core/flutter_chat_core.dart' as core;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flyer_chat_text_message/flyer_chat_text_message.dart';
import 'package:uuid/uuid.dart';
class RoomPage extends StatefulWidget {
const RoomPage({super.key, required this.room});
final ChatRoom room;
@override
State<RoomPage> createState() => _RoomPageState();
}
class _RoomPageState extends State<RoomPage> {
late final _controller = SupabaseChatController(
room: widget.room,
// Failures of background work (initial load, Realtime, mark as read).
onError: (error) => ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.message))),
);
String get _me => SupabaseChatCore.instance.loggedUserId!;
Future<core.User?> _resolveUser(String id) async =>
(widget.room.member(id) ??
await SupabaseChatCore.instance.user(uid: id))
?.toCoreUser();
Future<void> _send(String text) async {
try {
// Shown right away with status `sending`, reconciled with the stored
// row, marked `error` (see retryMessage) if the request fails.
await _controller.sendMessage(
core.Message.text(id: const Uuid().v7(), authorId: _me, text: text),
);
} on SupabaseChatCoreException catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.message)));
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(widget.room.name ?? 'Chat')),
body: Chat(
currentUserId: _me,
resolveUser: _resolveUser,
chatController: _controller,
onMessageSend: _send,
builders: core.Builders(
chatAnimatedListBuilder: (context, itemBuilder) => ChatAnimatedList(
itemBuilder: itemBuilder,
onEndReached: _controller.loadPreviousMessages,
),
textMessageBuilder:
(context, message, index, {required isSentByMe, groupStatus}) =>
FlyerChatTextMessage(message: message, index: index),
),
),
);
}
Generate message ids with const Uuid().v7(): the id is stored as-is (so the Realtime echo updates the optimistic copy instead of duplicating it), and v7 ids are time-ordered like the database's own chats.uuid_generate_v7().
What else the controller does:
| Member | Purpose |
|---|---|
pageSize (20), initialLoad, hasMoreMessages, loadPreviousMessages() |
Cursor-based pagination (stable while new messages arrive). |
autoMarkAsRead (true), markAsRead() |
Marks the room as read after the first page and on every incoming message. Pass false to decide yourself (e.g. only in foreground). |
sendMessage, retryMessage |
Optimistic send; failed messages keep status error. |
editTextMessage, deleteMessage |
Edit your text messages; delete yours (group admins: any). |
toggleReaction |
Add/remove your reaction (atomic RPC). |
pinnedMessages (ValueNotifier), setPinned |
Pinned messages kept up to date in real time. |
resolveMessage, jumpToMessage |
Resolve a quoted message (fetched if not loaded) and scroll to any message, loading older pages as needed. |
onTyping(), endTyping(), typingUsers, currentTypingUsers |
Typing indicator (see Realtime). |
inMemoryController |
The underlying InMemoryChatController, for advanced use. |
Call onTyping() on every keystroke of the composer, e.g. with your own TextEditingController passed to Composer through Builders.composerBuilder; sendMessage stops the indicator for you.
SupabaseChatCore at a glance
| Area | Methods |
|---|---|
| Rooms | rooms(), roomsChanges() + SupabaseChatCore.applyRoomChange(), room(id) (stream), getRoom(), createRoom(), createGroupRoom(), updateRoom(), deleteRoom(), markRoomAsRead(), pinRoom(), muteRoom(), unmuteRoom() |
| Members | addMembers(), removeMember(), leaveRoom(), setMemberRole() |
| Users | users(filter:, excludeLoggedUser:), user(), updateUser(), uploadUserAvatar(), loggedUser, loggedUserId, refreshLoggedUser() |
| Messages | messages(roomId, before:, limit:), message(), pinnedMessages(), searchMessages(), sendMessage(), forwardMessages(), updateMessage(), toggleReaction(), setMessagePinned(), deleteMessage() |
| Storage | uploadAsset(), getAssetUrl(), httpSupabaseHeaders (to load authenticated URLs) |
| Presence | setPresenceStatus(), userOnlineStatus() |
| Session | closeSession() (before signing out), dispose() |
SupabaseChatCore.instance is the singleton bound to Supabase.instance.client. Create your own instance with SupabaseChatCore(client: ..., config: ...) to use another client (e.g. several users in one process, as the integration tests do), and dispose() it when done. Controllers and widgets accept a chatCore: parameter for that.
Forwarding messages
await SupabaseChatCore.instance.forwardMessages(fromRoom, selectedMessages, toRoomId);
The messages are sent to toRoomId as new messages of the logged-in user (a member of both rooms), oldest first. Each copy keeps content and metadata, drops reply, reactions, pin and edited mark, and stores where it comes from: read it with ChatForwardOrigin.of(message) (authorId, authorName, createdAt) to show a "Forwarded from ..." header. Forwarding a forwarded message keeps the first origin. The author's name is a snapshot, since the recipients may not be able to read the original author's profile. Attachments in the chat assets bucket are copied into the destination room's folder, so its members can read them; external URLs are kept as they are.
Signing out
await SupabaseChatCore.instance.closeSession();
await Supabase.instance.client.auth.signOut();
closeSession() sets the user offline and closes everything opened for them: room channels, presence watches and the roomsChanges(), room() and userOnlineStatus() streams, which complete. Call it right before signing out: after the sign out Realtime is re-authorized with the anon key, which private room channels reject, so they would report subscription errors instead of leaving cleanly. Signing out (or switching user) without it still closes the session when the auth state change arrives, and SupabaseChatRoomsController clears its rooms until the next loadFirstPage().
Error handling
Every public method throws a SupabaseChatCoreException subtype, with the original error in cause:
SupabaseChatCoreAuthException: no logged-in user;SupabaseChatCoreRequestException: the request failed (RLS, constraint, connectivity, Realtime subscription...);SupabaseChatCoreStorageException: an upload failed.
Scoping (workspaces, organizations)
SupabaseChatCoreConfig takes named parameters, all with defaults matching the SQL scripts (schema: 'chats', realtimeRoomChannelPrefix: 'chats:room:', chatAssetsBucket: 'chats_assets', userAvatarsBucket: 'chats_user_avatar', ...). Besides names, it offers four hooks to keep the chats of different scopes apart:
SupabaseChatCore.instance.setConfig(
const SupabaseChatCoreConfig().copyWith(
defaultRoomMetadata: () => {'workspaceId': currentWorkspaceId},
roomsQueryFilter: (q) => q.eq('metadata->>workspaceId', currentWorkspaceId),
roomFilter: (room) => room.metadata?['workspaceId'] == currentWorkspaceId,
usersQueryFilter: (q) => q.eq('metadata->>workspaceId', currentWorkspaceId),
),
);
defaultRoomMetadatais merged into every new room; direct rooms are de-duplicated per pair and per these metadata entries, so each scope gets its own direct room.roomsQueryFilter/usersQueryFilternarrow every rooms / users query.roomFilterdrops rooms received over Realtime that don't belong to the scope (keep it consistent withroomsQueryFilter).usersViewNameletsusers()/user()read from a view (e.g. only users sharing a workspace with the caller) instead ofchats.users.
Hooks run on every call, so they can read state that changes at runtime. They only narrow what the client asks for: if the scope is a security boundary, enforce the same rule with RLS. If you rename tables, views, buckets or the channel prefix, update the SQL scripts accordingly.
Realtime and typing indicator
| What | Transport |
|---|---|
| Message inserts/updates/deletes | Private Broadcast channel chats:room:<roomId>, sent by the chats.broadcast_message_change trigger (realtime.broadcast_changes). |
| Typing indicator | Same channel, Broadcast events sent by clients. |
| Rooms list, single room | Postgres Changes on chats.rooms and chats.room_members. |
| Online status | Presence channel online-user-<uid>. |
- Postgres Changes can't be used for messages:
chats.messagesis partitioned and Postgres Changes reports rows under their physical partition, so a subscription on the parent table never matches. Broadcast also authorizes once at join time instead of checking RLS per change per subscriber. - Room channels are private: RLS policies on
realtime.messagesonly let room members join (receive) and send. One channel per room is shared by every controller open on it; a channel refused as unauthorized is released and not retried. - The typing indicator moved from Presence to Broadcast (#38): Presence
track()calls are rate-limited and the channel was dropped while typing. Each client sendstyping: trueat most every 2 s,typing: falseafter 3 s of inactivity (or on send), and receivers drop a user without a fresh event after 5 s, so a lost "stopped" event can't leave a stale indicator. Trade-off: Broadcast keeps no state, so someone opening the room while another user is already typing sees the indicator from that user's next event (at most 2 s later). - Online status: every
userOnlineStatus(uid)listener of the same user shares one Presence channel, closed when the last listener cancels; the stream emits the current status first.
Package widgets
UserOnlineStateObserver
Publishes the logged-in user's online status following the app lifecycle: online while in the foreground (on the web: while the browser window is focused), offline otherwise; going offline also stores lastSeen. It sets the user online as soon as it is mounted. Place it once, high in the widget tree.
MaterialApp(
home: const UserOnlineStateObserver(child: RoomsPage()),
);
UserOnlineStatusWidget
Rebuilds with the online/offline status of a user, e.g. to show a dot on an avatar:
UserOnlineStatusWidget(
uid: user.id,
builder: (status) => Icon(
Icons.circle,
size: 10,
color: status == UserOnlineStatus.online ? Colors.green : Colors.grey,
),
);
Both widgets take an optional chatCore: (defaults to SupabaseChatCore.instance).
RLS (Row level security)
03_database_policy.sql and 04_storage.sql set up the following rules. "Admin" means a member with role admin in a group room; in a direct room both members have the same rights.
Tables
| Table | INSERT | SELECT | UPDATE | DELETE |
|---|---|---|---|---|
chats.users |
Nobody (created by the sign-up trigger) | Authenticated users | The user himself | Nobody (deleted with auth.users) |
chats.rooms |
Nobody (only through chats.create_room) |
Members | Admins | Admins |
chats.room_members |
Group admins, group rooms only (no self-join) | Members of the same room | Admins (roles) | Yourself (leave) or admins (remove) |
chats.messages |
Members, as themselves only (authorId = auth.uid()) |
Members | The author | The author, or a group admin |
realtime.messages (topics chats:room:*) |
Members (Broadcast only) | Members (join the channel) | – | – |
UPDATE policies state their rule once, in USING: without WITH CHECK, Postgres applies it to the new row too, so a row can't be moved out of the rule.
Operations touching rows of other users go through SECURITY DEFINER RPCs that check membership themselves:
| RPC | Who |
|---|---|
chats.create_room(p_type, p_user_ids, p_name, p_image_url, p_metadata, p_creator_role) |
Any authenticated user (the caller is always a member) |
chats.mark_room_as_read(p_room_id) |
Members |
chats.toggle_message_reaction(p_message_id, p_reaction) |
Members |
chats.pin_room(p_room_id, p_pinned), chats.mute_room(p_room_id, p_until) |
Members (own preference) |
chats.set_message_pinned(p_message_id, p_pinned) |
Admins (either member of a direct room) |
Storage buckets
Both buckets are private: files are served through authenticated URLs (getAssetUrl, loaded with httpSupabaseHeaders).
| Bucket | INSERT | SELECT | UPDATE | DELETE |
|---|---|---|---|---|
chats_assets (<roomId>/<file>) |
Room members | Room members | Room members | Room members |
chats_user_avatar (<uid>/<file>) |
The user himself | Authenticated users | The user himself | The user himself |
Attachments uploaded by 1.x live under the old numeric room id folder: they stay readable through chats.room_id_migration_map until example/utils/migrate_storage_uuids.sh/.ps1 moves them to the room uuid folder (see MIGRATION.md).
Media cleanup
Deleting a message deletes its row immediately. When it referenced a Storage file, the chats.queue_media_for_deletion trigger records the object in chats.media_pending_deletion (closed to clients), because SQL alone can't delete objects from the storage backend. The cleanup-media Edge Function drains that queue (500 objects per call) through the Storage API.
- Deploy it:
supabase functions deploy cleanup-media. - Store the project URL and a service role key in Vault, once:
select vault.create_secret('https://<project-ref>.supabase.co', 'chats_project_url');
select vault.create_secret('<service-role-key>', 'chats_service_role_key');
- Run
example/utils/sql/06_cleanup_media_schedule.sql: it schedules the call every 15 minutes withpg_cron+pg_net, only when the queue isn't empty.
Without it nothing breaks: deleted messages' files simply stay in the bucket.
Testing and development
| Suite | Command |
|---|---|
| Unit tests (models, row mapping) | flutter test test/unit |
| Integration tests against a local Supabase stack | supabase start once, then tool/test_integration.sh |
| Database behaviour tests (pgTAP) | tool/local_db.sh && supabase test db |
tool/local_db.shresets the local database, applies the SQL scripts exactly like end users do and restarts the Realtime container (after a reset its Postgres Changes poller can stay degraded).--no-resetapplies them on top of the current database, to check they are idempotent.tool/test_integration.shrunstool/local_db.sh, reads the local URL and keys fromsupabase statusand runsflutter test test/integration --tags integration. Pass--no-resetto keep the database.tool/v1_fixture.shrebuilds the local database in the 1.x shape (scripts from themainbranch,V1_REFto change it) with sample data; follow it withtool/local_db.sh --no-resetto exercise the migration.tool/seed_demo.shseeds demo users and conversations (incl. a voice message): sign in asalice@demo.dev/Demo-password-1(alsobob@,carol@,dave@,erin@demo.dev).
Example app
The example is a Telegram-like chat built on this package and flutter_chat_ui: see example/README.md to run it.
| Feature | Web | Android | iOS | Windows | macOS | Linux |
|---|---|---|---|---|---|---|
| Sign up / sign in | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Rooms list (pagination, search) | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Pin / mute chats, unread badges | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Create direct room | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Create group room | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Group info and member management | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Search users | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Chat screen, messages pagination | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Replies (swipe), edit, delete | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Reactions, pinned messages | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Read receipts | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Search messages | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Upload / preview image | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Upload / download file | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Voice messages with waveform | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Typing indicator | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Users online status | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Profile and avatar | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Light / dark theme | ✅ | ✅ | 🟡 | ✅ | 🟡 | 🟡 |
| Two-pane layout on wide screens | ✅ | 🟡 | 🟡 | ✅ | 🟡 | 🟡 |
| Channel rooms | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 |
- ✅ Tested
- 🟡 Supported but not tested
- 🚧 Planned
Roadmap
Chat room groups✅ DoneSending audio messages✅ DoneError handling✅ DoneSearch messages✅ Done- Chat room channels (broadcast-only rooms)
- Push notifications example (database trigger + Edge Function)
- Link preview fetching
- Improve documentation
Contributing
Please read our Contributing Guide before submitting a pull request to the project.
Code of Conduct
Flyer Chat has adopted the Contributor Covenant as its Code of Conduct, and we expect project participants to adhere to it. Please read the full text so that you can understand what actions will and will not be tolerated.
License
Licensed under the Apache License, Version 2.0
Made with ❤️ by InsideApp · Documentation · GitHub
Libraries
- flutter_supabase_chat_core
- Supabase backend for chat apps built on
flutter_chat_core/flutter_chat_ui: rooms, members, messages, attachments, read receipts, reactions, typing indicators and online status, over Postgres + RLS and Supabase Realtime.