惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog RSS Feed
宝玉的分享
宝玉的分享
腾讯CDC
博客园_首页
T
Tailwind CSS Blog
月光博客
月光博客
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
SecWiki News
SecWiki News
美团技术团队
P
Privacy International News Feed
H
Help Net Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
Y
Y Combinator Blog
D
DataBreaches.Net
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
S
Schneier on Security
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
Forbes - Security
Forbes - Security
D
Docker
T
Tenable Blog
S
Secure Thoughts
雷峰网
雷峰网
S
Security @ Cisco Blogs
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Flutter Authorization with RLS
Tyler Shukert · 2022-11-22 · via Supabase Blog

Flutter Authorization with RLS

This article is the second part of the Flutter tutorial series. During the series, you will learn how to build cross-platform apps without worrying about the backend.

In this article, I will show you how you can make a secure chat application by introducing authorization to the basic chat app that we created previously.

We will store the chat data on Supabase. Supabase utilizes the built in authorization mechanism of PostgreSQL called Row Level Security or RLS to prevent unauthorized access from accessing or writing data to your database. RLS allows developers to define row-by-row conditions that evaluate to either true or false to either allow the access or deny it. We will take a look at more specific examples of authorization using RLS throughout this article.

What we created in the previous article#

Before we jump in, let's go over what we built in the previous article, because we will be building on top of it. If you have not gone through it, I recommend you to go check it out.

In the previous article, we created a basic real-time chat application. Users will register or sign in using an email address and password. Once they are signed in, they are taken to a chat page, where they can view and send messages to everyone in the app. There are no Chat rooms, and everyone's messages were sent to the same chat room.

You can also find a complete code example here to follow along.

The app will allow us to have 1 on 1 chat with other users in the app. To enable this, we will introduce a new rooms page. The rooms page serves two purposes here, one is to initiate a conversation with other users, and the other is to display existing chat rooms. At the top of the app, we see a list of other users' icons. A user can tap the icon to start a 1 on 1 conversation. Below the icons, there is a list of rooms that the user is a part of.

Install additional dependencies#

We will install flutter_bloc for state management. Introducing a state management solution will allow us to handle the shared message and profile data efficiently between the rooms page and the chats page. We can use any state management solution for this, but we are going with bloc in this example. Add the following in your pubspec.yaml file to install flutter_bloc in your app.

Modifying the table schema#

Since the app has evolved, we also need to update our table schema. In order to store rooms data, we will add a rooms table. We will also modify the messages table to add a foreign key constraint to the rooms table so that we can tell which message belongs to which room.

We will also introduce a create_new_room function, which is a database function that handles chat room creation. It knows to create a new room if a chat room with the two users does not exist yet, or to just return the room ID if it already exists.


_60

-- *** Table definitions ***

_60

_60

create table if not exists public.rooms (

_60

id uuid not null primary key default gen_random_uuid(),

_60

created_at timestamp with time zone default timezone('utc' :: text, now()) not null

_60

);

_60

comment on table public.rooms is 'Holds chat rooms';

_60

_60

create table if not exists public.room_participants (

_60

profile_id uuid references public.profiles(id) on delete cascade not null,

_60

room_id uuid references public.rooms(id) on delete cascade not null,

_60

created_at timestamp with time zone default timezone('utc' :: text, now()) not null,

_60

primary key (profile_id, room_id)

_60

);

_60

comment on table public.room_participants is 'Relational table of users and rooms.';

_60

_60

alter table public.messages

_60

add column room_id uuid references public.rooms(id) on delete cascade not null;

_60

_60

-- *** Add tables to the publication to enable realtime ***

_60

_60

alter publication supabase_realtime add table public.room_participants;

_60

_60

-- Creates a new room with the user and another user in it.

_60

-- Will return the room_id of the created room

_60

-- Will return a room_id if there were already a room with those participants

_60

create or replace function create_new_room(other_user_id uuid) returns uuid as $$

_60

declare

_60

new_room_id uuid;

_60

begin

_60

-- Check if room with both participants already exist

_60

with rooms_with_profiles as (

_60

select room_id, array_agg(profile_id) as participants

_60

from room_participants

_60

group by room_id

_60

)

_60

select room_id

_60

into new_room_id

_60

from rooms_with_profiles

_60

where create_new_room.other_user_id=any(participants)

_60

and auth.uid()=any(participants);

_60

_60

_60

if not found then

_60

-- Create a new room

_60

insert into public.rooms default values

_60

returning id into new_room_id;

_60

_60

-- Insert the caller user into the new room

_60

insert into public.room_participants (profile_id, room_id)

_60

values (auth.uid(), new_room_id);

_60

_60

-- Insert the other_user user into the new room

_60

insert into public.room_participants (profile_id, room_id)

_60

values (other_user_id, new_room_id);

_60

end if;

_60

_60

return new_room_id;

_60

end

_60

$$ language plpgsql security definer;


Something we skipped in the previous article was sending confirmation emails to users when they signup. Since today is about security, let's properly send confirmation emails to people who signup.

When we send confirmation emails, the users need to be brought back to the app somehow. Since supabase_flutter has a mechanism to detect and handle deep links, we will register a io.supabase.chat://login as our deep link for the app and bring the users back after confirming their email address.

For iOS we edit the info.plist file to register the deep link.


_20

<!-- ... other tags -->

_20

<plist>

_20

<dict>

_20

<!-- ... other tags -->

_20

_20

<!-- Add this array for Deep Links -->

_20

<key>CFBundleURLTypes</key>

_20

<array>

_20

<dict>

_20

<key>CFBundleTypeRole</key>

_20

<string>Editor</string>

_20

<key>CFBundleURLSchemes</key>

_20

<array>

_20

<string>io.supabase.chat</string>

_20

</array>

_20

</dict>

_20

</array>

_20

<!-- ... other tags -->

_20

</dict>

_20

</plist>


For Android we edit the AndroidManifest.xml to register the deep link.


_20

<manifest ...>

_20

<!-- ... other tags -->

_20

<application ...>

_20

<activity ...>

_20

<!-- ... other tags -->

_20

_20

<!-- Add this intent-filter for Deep Links -->

_20

<intent-filter>

_20

<action android:name="android.intent.action.VIEW" />

_20

<category android:name="android.intent.category.DEFAULT" />

_20

<category android:name="android.intent.category.BROWSABLE" />

_20

<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->

_20

<data

_20

android:scheme="io.supabase.chat"

_20

android:host="login" />

_20

</intent-filter>

_20

_20

</activity>

_20

</application>

_20

</manifest>


We also need to set the deep link in our Supabase dashboard. Go to Authentication > URL Configuration in your dashboard and add io.supabase.chat://login as one of the redirect URLs.

And that is it for deep link configuration.

Building out the main application#

Step1: Create rooms page#

The rooms page will load two types of data, recently added users and a list of rooms that the user belongs to. We will be using bloc to load these two types of data and display them on the rooms page.

Let's start out by creating states for the rooms page. The rooms page would have four different states, loading, loaded, empty, and error. We will display different UI on the rooms page depending on what state it is. Satrt by defining the Room model. Create a lib/models/room.dart file and add the following code.


_50

import 'package:my_chat_app/models/message.dart';

_50

_50

class Room {

_50

Room({

_50

required this.id,

_50

required this.createdAt,

_50

required this.otherUserId,

_50

this.lastMessage,

_50

});

_50

_50

/// ID of the room

_50

final String id;

_50

_50

/// Date and time when the room was created

_50

final DateTime createdAt;

_50

_50

/// ID of the user who the user is talking to

_50

final String otherUserId;

_50

_50

/// Latest message submitted in the room

_50

final Message? lastMessage;

_50

_50

Map<String, dynamic> toMap() {

_50

return {

_50

'id': id,

_50

'createdAt': createdAt.millisecondsSinceEpoch,

_50

};

_50

}

_50

_50

/// Creates a room object from room_participants table

_50

Room.fromRoomParticipants(Map<String, dynamic> map)

_50

: id = map['room_id'],

_50

otherUserId = map['profile_id'],

_50

createdAt = DateTime.parse(map['created_at']),

_50

lastMessage = null;

_50

_50

Room copyWith({

_50

String? id,

_50

DateTime? createdAt,

_50

String? otherUserId,

_50

Message? lastMessage,

_50

}) {

_50

return Room(

_50

id: id ?? this.id,

_50

createdAt: createdAt ?? this.createdAt,

_50

otherUserId: otherUserId ?? this.otherUserId,

_50

lastMessage: lastMessage ?? this.lastMessage,

_50

);

_50

}

_50

}


We will proceed with defining the states for the rooms page. Create lib/cubit/rooms/rooms_state.dart file and paste the following code. You may see some errors, but we will take care of them in the next step.


_28

part of 'rooms_cubit.dart';

_28

_28

@immutable

_28

abstract class RoomState {}

_28

_28

class RoomsLoading extends RoomState {}

_28

_28

class RoomsLoaded extends RoomState {

_28

final List<Profile> newUsers;

_28

final List<Room> rooms;

_28

_28

RoomsLoaded({

_28

required this.rooms,

_28

required this.newUsers,

_28

});

_28

}

_28

_28

class RoomsEmpty extends RoomState {

_28

final List<Profile> newUsers;

_28

_28

RoomsEmpty({required this.newUsers});

_28

}

_28

_28

class RoomsError extends RoomState {

_28

final String message;

_28

_28

RoomsError(this.message);

_28

}


Now that we have the states defined, we will create rooms_cubit. A cubit is a class within the flutter_bloc library where we will make requests to Supabase to get the data and transform them into states and emit them to the UI widgets. Let's create a lib/cubit/rooms/rooms_cubit.dart file and complete the cubit.


_140

import 'dart:async';

_140

_140

import 'package:flutter/material.dart';

_140

import 'package:flutter_bloc/flutter_bloc.dart';

_140

import 'package:my_chat_app/cubits/profiles/profiles_cubit.dart';

_140

import 'package:my_chat_app/models/profile.dart';

_140

import 'package:my_chat_app/models/message.dart';

_140

import 'package:my_chat_app/models/room.dart';

_140

import 'package:my_chat_app/utils/constants.dart';

_140

_140

part 'rooms_state.dart';

_140

_140

class RoomCubit extends Cubit<RoomState> {

_140

RoomCubit() : super(RoomsLoading());

_140

_140

final Map<String, StreamSubscription<Message?>>

_140

_messageSubscriptions = {};

_140

_140

late final String _myUserId;

_140

_140

/// List of new users of the app for the user to start talking to

_140

late final List<Profile> _newUsers;

_140

_140

/// List of rooms

_140

List<Room> _rooms = [];

_140

StreamSubscription<List<Map<String, dynamic>>>?

_140

_rawRoomsSubscription;

_140

bool _haveCalledGetRooms = false;

_140

_140

Future<void> initializeRooms(BuildContext context) async {

_140

if (_haveCalledGetRooms) {

_140

return;

_140

}

_140

_haveCalledGetRooms = true;

_140

_140

_myUserId = supabase.auth.currentUser!.id;

_140

_140

late final List data;

_140

_140

try {

_140

data = await supabase

_140

.from('profiles')

_140

.select()

_140

.not('id', 'eq', _myUserId)

_140

.order('created_at')

_140

.limit(12);

_140

} catch (_) {

_140

emit(RoomsError('Error loading new users'));

_140

}

_140

_140

final rows = List<Map<String, dynamic>>.from(data);

_140

_newUsers = rows.map(Profile.fromMap).toList();

_140

_140

/// Get realtime updates on rooms that the user is in

_140

_rawRoomsSubscription =

_140

supabase.from('room_participants').stream(

_140

primaryKey: ['room_id', 'profile_id'],

_140

).listen((participantMaps) async {

_140

if (participantMaps.isEmpty) {

_140

emit(RoomsEmpty(newUsers: _newUsers));

_140

return;

_140

}

_140

_140

_rooms = participantMaps

_140

.map(Room.fromRoomParticipants)

_140

.where((room) => room.otherUserId != _myUserId)

_140

.toList();

_140

for (final room in _rooms) {

_140

_getNewestMessage(

_140

context: context, roomId: room.id);

_140

BlocProvider.of<ProfilesCubit>(context)

_140

.getProfile(room.otherUserId);

_140

}

_140

emit(RoomsLoaded(

_140

newUsers: _newUsers,

_140

rooms: _rooms,

_140

));

_140

}, onError: (error) {

_140

emit(RoomsError('Error loading rooms'));

_140

});

_140

}

_140

_140

// Setup listeners to listen to the most recent message in each room

_140

void _getNewestMessage({

_140

required BuildContext context,

_140

required String roomId,

_140

}) {

_140

_messageSubscriptions[roomId] = supabase

_140

.from('messages')

_140

.stream(primaryKey: ['id'])

_140

.eq('room_id', roomId)

_140

.order('created_at')

_140

.limit(1)

_140

.map<Message?>(

_140

(data) => data.isEmpty

_140

? null

_140

: Message.fromMap(

_140

map: data.first,

_140

myUserId: _myUserId,

_140

),

_140

)

_140

.listen((message) {

_140

final index = _rooms

_140

.indexWhere((room) => room.id == roomId);

_140

_rooms[index] =

_140

_rooms[index].copyWith(lastMessage: message);

_140

_rooms.sort((a, b) {

_140

/// Sort according to the last message

_140

/// Use the room createdAt when last message is not available

_140

final aTimeStamp = a.lastMessage != null

_140

? a.lastMessage!.createdAt

_140

: a.createdAt;

_140

final bTimeStamp = b.lastMessage != null

_140

? b.lastMessage!.createdAt

_140

: b.createdAt;

_140

return bTimeStamp.compareTo(aTimeStamp);

_140

});

_140

if (!isClosed) {

_140

emit(RoomsLoaded(

_140

newUsers: _newUsers,

_140

rooms: _rooms,

_140

));

_140

}

_140

});

_140

}

_140

_140

/// Creates or returns an existing roomID of both participants

_140

Future<String> createRoom(String otherUserId) async {

_140

final data = await supabase.rpc('create_new_room',

_140

params: {'other_user_id': otherUserId});

_140

emit(RoomsLoaded(rooms: _rooms, newUsers: _newUsers));

_140

return data as String;

_140

}

_140

_140

@override

_140

Future<void> close() {

_140

_rawRoomsSubscription?.cancel();

_140

return super.close();

_140

}

_140

}


Now that we have the states and cubit to power our rooms page, it's time to create the RoomsPage.

We have two list views, one horizontal list view to display other users, and one vertical list views with list tiles representing each room that the user is a part of. We will create a lib/pages/rooms_page.dart file with the following content.


_187

import 'package:flutter/material.dart';

_187

import 'package:flutter_bloc/flutter_bloc.dart';

_187

import 'package:my_chat_app/cubits/profiles/profiles_cubit.dart';

_187

_187

import 'package:my_chat_app/cubits/rooms/rooms_cubit.dart';

_187

import 'package:my_chat_app/models/profile.dart';

_187

import 'package:my_chat_app/pages/chat_page.dart';

_187

import 'package:my_chat_app/pages/register_page.dart';

_187

import 'package:my_chat_app/utils/constants.dart';

_187

import 'package:timeago/timeago.dart';

_187

_187

/// Displays the list of chat threads

_187

class RoomsPage extends StatelessWidget {

_187

const RoomsPage({Key? key}) : super(key: key);

_187

_187

static Route<void> route() {

_187

return MaterialPageRoute(

_187

builder: (context) => BlocProvider<RoomCubit>(

_187

create: (context) =>

_187

RoomCubit()..initializeRooms(context),

_187

child: const RoomsPage(),

_187

),

_187

);

_187

}

_187

_187

@override

_187

Widget build(BuildContext context) {

_187

return Scaffold(

_187

appBar: AppBar(

_187

title: const Text('Rooms'),

_187

actions: [

_187

TextButton(

_187

onPressed: () async {

_187

await supabase.auth.signOut();

_187

Navigator.of(context).pushAndRemoveUntil(

_187

RegisterPage.route(),

_187

(route) => false,

_187

);

_187

},

_187

child: const Text('Logout'),

_187

),

_187

],

_187

),

_187

body: BlocBuilder<RoomCubit, RoomState>(

_187

builder: (context, state) {

_187

if (state is RoomsLoading) {

_187

return preloader;

_187

} else if (state is RoomsLoaded) {

_187

final newUsers = state.newUsers;

_187

final rooms = state.rooms;

_187

return BlocBuilder<ProfilesCubit,

_187

ProfilesState>(

_187

builder: (context, state) {

_187

if (state is ProfilesLoaded) {

_187

final profiles = state.profiles;

_187

return Column(

_187

children: [

_187

_NewUsers(newUsers: newUsers),

_187

Expanded(

_187

child: ListView.builder(

_187

itemCount: rooms.length,

_187

itemBuilder: (context, index) {

_187

final room = rooms[index];

_187

final otherUser =

_187

profiles[room.otherUserId];

_187

_187

return ListTile(

_187

onTap: () =>

_187

Navigator.of(context)

_187

.push(ChatPage.route(

_187

room.id)),

_187

leading: CircleAvatar(

_187

child: otherUser == null

_187

? preloader

_187

: Text(otherUser

_187

.username

_187

.substring(0, 2)),

_187

),

_187

title: Text(otherUser == null

_187

? 'Loading...'

_187

: otherUser.username),

_187

subtitle: room.lastMessage !=

_187

null

_187

? Text(

_187

room.lastMessage!

_187

.content,

_187

maxLines: 1,

_187

overflow: TextOverflow

_187

.ellipsis,

_187

)

_187

: const Text(

_187

'Room created'),

_187

trailing: Text(format(

_187

room.lastMessage

_187

?.createdAt ??

_187

room.createdAt,

_187

locale: 'en_short')),

_187

);

_187

},

_187

),

_187

),

_187

],

_187

);

_187

} else {

_187

return preloader;

_187

}

_187

},

_187

);

_187

} else if (state is RoomsEmpty) {

_187

final newUsers = state.newUsers;

_187

return Column(

_187

children: [

_187

_NewUsers(newUsers: newUsers),

_187

const Expanded(

_187

child: Center(

_187

child: Text(

_187

'Start a chat by tapping on available users'),

_187

),

_187

),

_187

],

_187

);

_187

} else if (state is RoomsError) {

_187

return Center(child: Text(state.message));

_187

}

_187

throw UnimplementedError();

_187

},

_187

),

_187

);

_187

}

_187

}

_187

_187

class _NewUsers extends StatelessWidget {

_187

const _NewUsers({

_187

Key? key,

_187

required this.newUsers,

_187

}) : super(key: key);

_187

_187

final List<Profile> newUsers;

_187

_187

@override

_187

Widget build(BuildContext context) {

_187

return SingleChildScrollView(

_187

padding: const EdgeInsets.symmetric(vertical: 8),

_187

scrollDirection: Axis.horizontal,

_187

child: Row(

_187

children: newUsers

_187

.map<Widget>((user) => InkWell(

_187

onTap: () async {

_187

try {

_187

final roomId =

_187

await BlocProvider.of<RoomCubit>(

_187

context)

_187

.createRoom(user.id);

_187

Navigator.of(context)

_187

.push(ChatPage.route(roomId));

_187

} catch (_) {

_187

context.showErrorSnackBar(

_187

message:

_187

'Failed creating a new room');

_187

}

_187

},

_187

child: Padding(

_187

padding: const EdgeInsets.all(8.0),

_187

child: SizedBox(

_187

width: 60,

_187

child: Column(

_187

children: [

_187

CircleAvatar(

_187

child: Text(user.username

_187

.substring(0, 2)),

_187

),

_187

const SizedBox(height: 8),

_187

Text(

_187

user.username,

_187

maxLines: 1,

_187

overflow: TextOverflow.ellipsis,

_187

),

_187

],

_187

),

_187

),

_187

),

_187

))

_187

.toList(),

_187

),

_187

);

_187

}

_187

}


You may see some errors, but they will go away once we edit the chat page!

Step 2: Modify the chat page to load messages in the room#

Our ChatPage will have a similar layout as the previous one, but will only display messages sent to a single room. We will start by creating MessagesState. The messages page will also have four different states, loading, loaded, empty, and error. Create a lib/cubits/chat/chat_state.dart file with the following code.


_18

part of 'chat_cubit.dart';

_18

_18

@immutable

_18

abstract class ChatState {}

_18

_18

class ChatInitial extends ChatState {}

_18

_18

class ChatLoaded extends ChatState {

_18

ChatLoaded(this.messages);

_18

final List<Message> messages;

_18

}

_18

_18

class ChatEmpty extends ChatState {}

_18

_18

class ChatError extends ChatState {

_18

ChatError(this.message);

_18

final String message;

_18

}


Now let's create chat cubit to retrieve the data from our database and emit it as states. Create a lib/cubits/chat/chat_cubit.dart file and paste the following.


_72

import 'dart:async';

_72

_72

import 'package:bloc/bloc.dart';

_72

import 'package:meta/meta.dart';

_72

import 'package:my_chat_app/models/message.dart';

_72

import 'package:my_chat_app/utils/constants.dart';

_72

_72

part 'chat_state.dart';

_72

_72

class ChatCubit extends Cubit<ChatState> {

_72

ChatCubit() : super(ChatInitial());

_72

_72

StreamSubscription<List<Message>>? _messagesSubscription;

_72

List<Message> _messages = [];

_72

_72

late final String _roomId;

_72

late final String _myUserId;

_72

_72

void setMessagesListener(String roomId) {

_72

_roomId = roomId;

_72

_72

_myUserId = supabase.auth.currentUser!.id;

_72

_72

_messagesSubscription = supabase

_72

.from('messages')

_72

.stream(primaryKey: ['id'])

_72

.eq('room_id', roomId)

_72

.order('created_at')

_72

.map<List<Message>>(

_72

(data) => data

_72

.map<Message>(

_72

(row) => Message.fromMap(map: row, myUserId: _myUserId))

_72

.toList(),

_72

)

_72

.listen((messages) {

_72

_messages = messages;

_72

if (_messages.isEmpty) {

_72

emit(ChatEmpty());

_72

} else {

_72

emit(ChatLoaded(_messages));

_72

}

_72

});

_72

}

_72

_72

Future<void> sendMessage(String text) async {

_72

/// Add message to present to the user right away

_72

final message = Message(

_72

id: 'new',

_72

roomId: _roomId,

_72

profileId: _myUserId,

_72

content: text,

_72

createdAt: DateTime.now(),

_72

isMine: true,

_72

);

_72

_messages.insert(0, message);

_72

emit(ChatLoaded(_messages));

_72

_72

try {

_72

await supabase.from('messages').insert(message.toMap());

_72

} catch (_) {

_72

emit(ChatError('Error submitting message.'));

_72

_messages.removeWhere((message) => message.id == 'new');

_72

emit(ChatLoaded(_messages));

_72

}

_72

}

_72

_72

@override

_72

Future<void> close() {

_72

_messagesSubscription?.cancel();

_72

return super.close();

_72

}

_72

}


Chat cubit is pretty simple. It sets a real-time listener to the database using the stream method and emits an empty state if there are no messages in the room, or emits a loaded state if there are messages.

Because we are using cubit, we need to modify the MessagesPage widget as well. Open lib/pages/chat_page.dart and let's update it.


_193

import 'package:flutter/material.dart';

_193

import 'package:flutter_bloc/flutter_bloc.dart';

_193

import 'package:my_chat_app/components/user_avatar.dart';

_193

import 'package:my_chat_app/cubits/chat/chat_cubit.dart';

_193

_193

import 'package:my_chat_app/models/message.dart';

_193

import 'package:my_chat_app/utils/constants.dart';

_193

import 'package:timeago/timeago.dart';

_193

_193

/// Page to chat with someone.

_193

///

_193

/// Displays chat bubbles as a ListView and TextField to enter new chat.

_193

class ChatPage extends StatelessWidget {

_193

const ChatPage({Key? key}) : super(key: key);

_193

_193

static Route<void> route(String roomId) {

_193

return MaterialPageRoute(

_193

builder: (context) => BlocProvider<ChatCubit>(

_193

create: (context) => ChatCubit()..setMessagesListener(roomId),

_193

child: const ChatPage(),

_193

),

_193

);

_193

}

_193

_193

@override

_193

Widget build(BuildContext context) {

_193

return Scaffold(

_193

appBar: AppBar(title: const Text('Chat')),

_193

body: BlocConsumer<ChatCubit, ChatState>(

_193

listener: (context, state) {

_193

if (state is ChatError) {

_193

context.showErrorSnackBar(message: state.message);

_193

}

_193

},

_193

builder: (context, state) {

_193

if (state is ChatInitial) {

_193

return preloader;

_193

} else if (state is ChatLoaded) {

_193

final messages = state.messages;

_193

return Column(

_193

children: [

_193

Expanded(

_193

child: ListView.builder(

_193

padding: const EdgeInsets.symmetric(vertical: 8),

_193

reverse: true,

_193

itemCount: messages.length,

_193

itemBuilder: (context, index) {

_193

final message = messages[index];

_193

return _ChatBubble(message: message);

_193

},

_193

),

_193

),

_193

const _MessageBar(),

_193

],

_193

);

_193

} else if (state is ChatEmpty) {

_193

return Column(

_193

children: const [

_193

Expanded(

_193

child: Center(

_193

child: Text('Start your conversation now :)'),

_193

),

_193

),

_193

_MessageBar(),

_193

],

_193

);

_193

} else if (state is ChatError) {

_193

return Center(child: Text(state.message));

_193

}

_193

throw UnimplementedError();

_193

},

_193

),

_193

);

_193

}

_193

}

_193

_193

/// Set of widget that contains TextField and Button to submit message

_193

class _MessageBar extends StatefulWidget {

_193

const _MessageBar({

_193

Key? key,

_193

}) : super(key: key);

_193

_193

@override

_193

State<_MessageBar> createState() => _MessageBarState();

_193

}

_193

_193

class _MessageBarState extends State<_MessageBar> {

_193

late final TextEditingController _textController;

_193

_193

@override

_193

Widget build(BuildContext context) {

_193

return Material(

_193

color: Theme.of(context).cardColor,

_193

child: Padding(

_193

padding: EdgeInsets.only(

_193

top: 8,

_193

left: 8,

_193

right: 8,

_193

bottom: MediaQuery.of(context).padding.bottom,

_193

),

_193

child: Row(

_193

children: [

_193

Expanded(

_193

child: TextFormField(

_193

keyboardType: TextInputType.text,

_193

maxLines: null,

_193

autofocus: true,

_193

controller: _textController,

_193

decoration: const InputDecoration(

_193

hintText: 'Type a message',

_193

border: InputBorder.none,

_193

focusedBorder: InputBorder.none,

_193

contentPadding: EdgeInsets.all(8),

_193

),

_193

),

_193

),

_193

TextButton(

_193

onPressed: () => _submitMessage(),

_193

child: const Text('Send'),

_193

),

_193

],

_193

),

_193

),

_193

);

_193

}

_193

_193

@override

_193

void initState() {

_193

_textController = TextEditingController();

_193

super.initState();

_193

}

_193

_193

@override

_193

void dispose() {

_193

_textController.dispose();

_193

super.dispose();

_193

}

_193

_193

void _submitMessage() async {

_193

final text = _textController.text;

_193

if (text.isEmpty) {

_193

return;

_193

}

_193

BlocProvider.of<ChatCubit>(context).sendMessage(text);

_193

_textController.clear();

_193

}

_193

}

_193

_193

class _ChatBubble extends StatelessWidget {

_193

const _ChatBubble({

_193

Key? key,

_193

required this.message,

_193

}) : super(key: key);

_193

_193

final Message message;

_193

_193

@override

_193

Widget build(BuildContext context) {

_193

List<Widget> chatContents = [

_193

if (!message.isMine) UserAvatar(userId: message.profileId),

_193

const SizedBox(width: 12),

_193

Flexible(

_193

child: Container(

_193

padding: const EdgeInsets.symmetric(

_193

vertical: 8,

_193

horizontal: 12,

_193

),

_193

decoration: BoxDecoration(

_193

color: message.isMine

_193

? Colors.grey[300]

_193

: Theme.of(context).primaryColor,

_193

borderRadius: BorderRadius.circular(8),

_193

),

_193

child: Text(message.content),

_193

),

_193

),

_193

const SizedBox(width: 12),

_193

Text(format(message.createdAt, locale: 'en_short')),

_193

const SizedBox(width: 60),

_193

];

_193

if (message.isMine) {

_193

chatContents = chatContents.reversed.toList();

_193

}

_193

return Padding(

_193

padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 18),

_193

child: Row(

_193

mainAxisAlignment:

_193

message.isMine ? MainAxisAlignment.end : MainAxisAlignment.start,

_193

children: chatContents,

_193

),

_193

);

_193

}

_193

}


Step 3: Handle user sign-up/ sign-in#

Because we have modified the setting of our Supabase to send a confirmation email, we need to make some modifications to the register page and login page as well.

The main change is how we handle navigation. Previously, we were able to navigate the user to ChatPage right after sign-in was complete. This would no longer work, as we now have to wait for the user to confirm their email address. In this case, we would want to listen to auth state of the user and navigate when the user is signed in with a session. This allows us to react when the user confirmed their email addresses.


_169

import 'dart:async';

_169

_169

import 'package:flutter/material.dart';

_169

import 'package:my_chat_app/pages/login_page.dart';

_169

import 'package:my_chat_app/pages/rooms_page.dart';

_169

import 'package:my_chat_app/utils/constants.dart';

_169

import 'package:supabase_flutter/supabase_flutter.dart';

_169

_169

class RegisterPage extends StatefulWidget {

_169

const RegisterPage(

_169

{Key? key, required this.isRegistering})

_169

: super(key: key);

_169

_169

static Route<void> route({bool isRegistering = false}) {

_169

return MaterialPageRoute(

_169

builder: (context) =>

_169

RegisterPage(isRegistering: isRegistering),

_169

);

_169

}

_169

_169

final bool isRegistering;

_169

_169

@override

_169

State<RegisterPage> createState() => _RegisterPageState();

_169

}

_169

_169

class _RegisterPageState extends State<RegisterPage> {

_169

final bool _isLoading = false;

_169

_169

final _formKey = GlobalKey<FormState>();

_169

_169

final _emailController = TextEditingController();

_169

final _passwordController = TextEditingController();

_169

final _usernameController = TextEditingController();

_169

_169

late final StreamSubscription<AuthState>

_169

_authSubscription;

_169

_169

@override

_169

void initState() {

_169

super.initState();

_169

_169

bool haveNavigated = false;

_169

// Listen to auth state to redirect user when the user clicks on confirmation link

_169

_authSubscription =

_169

supabase.auth.onAuthStateChange.listen((data) {

_169

final session = data.session;

_169

if (session != null && !haveNavigated) {

_169

haveNavigated = true;

_169

Navigator.of(context)

_169

.pushReplacement(RoomsPage.route());

_169

}

_169

});

_169

}

_169

_169

@override

_169

void dispose() {

_169

super.dispose();

_169

_169

// Dispose subscription when no longer needed

_169

_authSubscription.cancel();

_169

}

_169

_169

Future<void> _signUp() async {

_169

final isValid = _formKey.currentState!.validate();

_169

if (!isValid) {

_169

return;

_169

}

_169

final email = _emailController.text;

_169

final password = _passwordController.text;

_169

final username = _usernameController.text;

_169

try {

_169

await supabase.auth.signUp(

_169

email: email,

_169

password: password,

_169

data: {'username': username},

_169

emailRedirectTo: 'io.supabase.chat://login',

_169

);

_169

context.showSnackBar(

_169

message:

_169

'Please check your inbox for confirmation email.');

_169

} on AuthException catch (error) {

_169

context.showErrorSnackBar(message: error.message);

_169

} catch (error) {

_169

debugPrint(error.toString());

_169

context.showErrorSnackBar(

_169

message: unexpectedErrorMessage);

_169

}

_169

}

_169

_169

@override

_169

Widget build(BuildContext context) {

_169

return Scaffold(

_169

appBar: AppBar(

_169

title: const Text('Register'),

_169

),

_169

body: Form(

_169

key: _formKey,

_169

child: ListView(

_169

padding: formPadding,

_169

children: [

_169

TextFormField(

_169

controller: _emailController,

_169

decoration: const InputDecoration(

_169

label: Text('Email'),

_169

),

_169

validator: (val) {

_169

if (val == null || val.isEmpty) {

_169

return 'Required';

_169

}

_169

return null;

_169

},

_169

keyboardType: TextInputType.emailAddress,

_169

),

_169

spacer,

_169

TextFormField(

_169

controller: _passwordController,

_169

obscureText: true,

_169

decoration: const InputDecoration(

_169

label: Text('Password'),

_169

),

_169

validator: (val) {

_169

if (val == null || val.isEmpty) {

_169

return 'Required';

_169

}

_169

if (val.length < 6) {

_169

return '6 characters minimum';

_169

}

_169

return null;

_169

},

_169

),

_169

spacer,

_169

TextFormField(

_169

controller: _usernameController,

_169

decoration: const InputDecoration(

_169

label: Text('Username'),

_169

),

_169

validator: (val) {

_169

if (val == null || val.isEmpty) {

_169

return 'Required';

_169

}

_169

final isValid =

_169

RegExp(r'^[A-Za-z0-9_]{3,24}$')

_169

.hasMatch(val);

_169

if (!isValid) {

_169

return '3-24 long with alphanumeric or underscore';

_169

}

_169

return null;

_169

},

_169

),

_169

spacer,

_169

ElevatedButton(

_169

onPressed: _isLoading ? null : _signUp,

_169

child: const Text('Register'),

_169

),

_169

spacer,

_169

TextButton(

_169

onPressed: () {

_169

Navigator.of(context)

_169

.push(LoginPage.route());

_169

},

_169

child:

_169

const Text('I already have an account'))

_169

],

_169

),

_169

),

_169

);

_169

}

_169

}


Login page becomes simpler. All it is doing is taking a user's email and password and logging them in. It is not doing any navigation whatsoever. This is because LoginPage is navigated on top of RegisterPage, the auth state listener on RegisterPage is still active, and therefore can take care of the navigation.


_80

import 'package:flutter/material.dart';

_80

import 'package:my_chat_app/utils/constants.dart';

_80

import 'package:supabase_flutter/supabase_flutter.dart';

_80

_80

class LoginPage extends StatefulWidget {

_80

const LoginPage({Key? key}) : super(key: key);

_80

_80

static Route<void> route() {

_80

return MaterialPageRoute(

_80

builder: (context) => const LoginPage());

_80

}

_80

_80

@override

_80

_LoginPageState createState() => _LoginPageState();

_80

}

_80

_80

class _LoginPageState extends State<LoginPage> {

_80

bool _isLoading = false;

_80

final _emailController = TextEditingController();

_80

final _passwordController = TextEditingController();

_80

_80

Future<void> _signIn() async {

_80

setState(() {

_80

_isLoading = true;

_80

});

_80

try {

_80

await supabase.auth.signInWithPassword(

_80

email: _emailController.text,

_80

password: _passwordController.text,

_80

);

_80

} on AuthException catch (error) {

_80

context.showErrorSnackBar(message: error.message);

_80

} catch (_) {

_80

context.showErrorSnackBar(

_80

message: unexpectedErrorMessage);

_80

}

_80

if (mounted) {

_80

setState(() {

_80

_isLoading = true;

_80

});

_80

}

_80

}

_80

_80

@override

_80

void dispose() {

_80

_emailController.dispose();

_80

_passwordController.dispose();

_80

super.dispose();

_80

}

_80

_80

@override

_80

Widget build(BuildContext context) {

_80

return Scaffold(

_80

appBar: AppBar(title: const Text('Sign In')),

_80

body: ListView(

_80

padding: formPadding,

_80

children: [

_80

TextFormField(

_80

controller: _emailController,

_80

decoration:

_80

const InputDecoration(labelText: 'Email'),

_80

keyboardType: TextInputType.emailAddress,

_80

),

_80

spacer,

_80

TextFormField(

_80

controller: _passwordController,

_80

decoration: const InputDecoration(

_80

labelText: 'Password'),

_80

obscureText: true,

_80

),

_80

spacer,

_80

ElevatedButton(

_80

onPressed: _isLoading ? null : _signIn,

_80

child: const Text('Login'),

_80

),

_80

],

_80

),

_80

);

_80

}

_80

}


Notice that I have not used bloc anywhere on the register or login page. I try to only use state management libraries for pages that have some complexity. Since both register and login pages are relatively simple, I am going with the good old setState.

We should also modify the splash page to redirect signed-in users to the RoomsPage by default.


_51

import 'package:flutter/material.dart';

_51

import 'package:my_chat_app/pages/register_page.dart';

_51

import 'package:my_chat_app/pages/rooms_page.dart';

_51

import 'package:my_chat_app/utils/constants.dart';

_51

import 'package:supabase_flutter/supabase_flutter.dart';

_51

_51

/// Page to redirect users to the appropriate page depending on the initial auth state

_51

class SplashPage extends StatefulWidget {

_51

const SplashPage({Key? key}) : super(key: key);

_51

_51

@override

_51

SplashPageState createState() => SplashPageState();

_51

}

_51

_51

class SplashPageState extends State<SplashPage> {

_51

@override

_51

void initState() {

_51

getInitialSession();

_51

super.initState();

_51

}

_51

_51

Future<void> getInitialSession() async {

_51

// quick and dirty way to wait for the widget to mount

_51

await Future.delayed(Duration.zero);

_51

_51

try {

_51

final session =

_51

await SupabaseAuth.instance.initialSession;

_51

if (session == null) {

_51

Navigator.of(context).pushAndRemoveUntil(

_51

RegisterPage.route(), (_) => false);

_51

} else {

_51

Navigator.of(context).pushAndRemoveUntil(

_51

RoomsPage.route(), (_) => false);

_51

}

_51

} catch (_) {

_51

context.showErrorSnackBar(

_51

message: 'Error occurred during session refresh',

_51

);

_51

Navigator.of(context).pushAndRemoveUntil(

_51

RegisterPage.route(), (_) => false);

_51

}

_51

}

_51

_51

@override

_51

Widget build(BuildContext context) {

_51

return const Scaffold(

_51

body: Center(child: CircularProgressIndicator()),

_51

);

_51

}

_51

}


Finally we implement those ProfilesCubit that you saw here and there throughout the code. This cubit will act as in memory cache of all the profiles data so that the app does not have to go fetch the same profiles every single time it needs it. Create profiles_state.dart and profiles_cubit.dart under lib/cubits/ and add the following code.


_14

part of 'profiles_cubit.dart';

_14

_14

@immutable

_14

abstract class ProfilesState {}

_14

_14

class ProfilesInitial extends ProfilesState {}

_14

_14

class ProfilesLoaded extends ProfilesState {

_14

ProfilesLoaded({

_14

required this.profiles,

_14

});

_14

_14

final Map<String, Profile?> profiles;

_14

}



_33

import 'dart:async';

_33

_33

import 'package:bloc/bloc.dart';

_33

import 'package:meta/meta.dart';

_33

import 'package:my_chat_app/models/profile.dart';

_33

import 'package:my_chat_app/utils/constants.dart';

_33

_33

part 'profiles_state.dart';

_33

_33

class ProfilesCubit extends Cubit<ProfilesState> {

_33

ProfilesCubit() : super(ProfilesInitial());

_33

_33

/// Map of app users cache in memory with profile_id as the key

_33

final Map<String, Profile?> _profiles = {};

_33

_33

Future<void> getProfile(String userId) async {

_33

if (_profiles[userId] != null) {

_33

return;

_33

}

_33

_33

final data = await supabase

_33

.from('profiles')

_33

.select()

_33

.match({'id': userId}).single();

_33

_33

if (data == null) {

_33

return;

_33

}

_33

_profiles[userId] = Profile.fromMap(data);

_33

_33

emit(ProfilesLoaded(profiles: _profiles));

_33

}

_33

}


We will make the ProfilesCubit accessible from anywhere in the app with the following code in main.dart file.


_36

import 'package:flutter/material.dart';

_36

import 'package:flutter_bloc/flutter_bloc.dart';

_36

import 'package:my_chat_app/cubits/profiles/profiles_cubit.dart';

_36

import 'package:my_chat_app/utils/constants.dart';

_36

import 'package:supabase_flutter/supabase_flutter.dart';

_36

import 'package:my_chat_app/pages/splash_page.dart';

_36

_36

Future<void> main() async {

_36

WidgetsFlutterBinding.ensureInitialized();

_36

_36

await Supabase.initialize(

_36

// TODO: Replace credentials with your own

_36

url: 'supabase_url',

_36

anonKey: 'supabase_anon_key',

_36

authCallbackUrlHostname: 'login',

_36

);

_36

_36

runApp(const MyApp());

_36

}

_36

_36

class MyApp extends StatelessWidget {

_36

const MyApp({Key? key}) : super(key: key);

_36

_36

@override

_36

Widget build(BuildContext context) {

_36

return BlocProvider<ProfilesCubit>(

_36

create: (context) => ProfilesCubit(),

_36

child: MaterialApp(

_36

title: 'SupaChat',

_36

debugShowCheckedModeBanner: false,

_36

theme: appTheme,

_36

home: const SplashPage(),

_36

),

_36

);

_36

}

_36

}


At this point, we seemingly have a complete app. But if we open the app right now, we will see every users' room along with all the messages that have ever been sent. This is because we have not set up Row Level Security to prevent users from accessing rooms that don't belong to them. There are two ways we can define Row Level Security policies in Supabase: with the GUI or through SQL. Today we will use SQL. Let's run the following SQL to set the security policy.


_34

-- Returns true if the signed in user is a participant of the room

_34

create or replace function is_room_participant(room_id uuid)

_34

returns boolean as $$

_34

select exists(

_34

select 1

_34

from room_participants

_34

where room_id = is_room_participant.room_id and profile_id = auth.uid()

_34

);

_34

$$ language sql security definer;

_34

_34

_34

-- *** Row level security polities ***

_34

_34

_34

alter table public.profiles enable row level security;

_34

create policy "Public profiles are viewable by everyone."

_34

on public.profiles for select using (true);

_34

_34

_34

alter table public.rooms enable row level security;

_34

create policy "Users can view rooms that they have joined"

_34

on public.rooms for select using (is_room_participant(id));

_34

_34

_34

alter table public.room_participants enable row level security;

_34

create policy "Participants of the room can view other participants."

_34

on public.room_participants for select using (is_room_participant(room_id));

_34

_34

_34

alter table public.messages enable row level security;

_34

create policy "Users can view messages on rooms they are in."

_34

on public.messages for select using (is_room_participant(room_id));

_34

create policy "Users can insert messages on rooms they are in."

_34

on public.messages for insert with check (is_room_participant(room_id) and profile_id = auth.uid());


Notice that we have created a handy is_room_participant function that will return whether a particular user is a participant or not in a specific room.

With the Row Level Security policies set up, our application is complete. We now have a real-time chat application with proper authentication and authorization in place.

Continuing from our previous article, we added proper authorization to our chat application using Row Level Security, which enabled us to add 1 on 1 chat feature. We used bloc for our state management solution. One thing we could have done differently if we were to write test codes was to pass the supabase instance as a parameter of the cubit so that we could write tests using the bloc_test package.

We could also explore some cool feature improvement. At the top of the rooms page, we are loading the newest created users to start a conversation. This is fine, but it only allows users to start a conversation with new users. We can for example update this to a list of users that are online at the same time. We can implement this using the presence feature of Supabase.