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;


Setup deep links#

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

#Security
Supabase Blog

Publisher

Originally by Tyler Shukert


0 Comments

Log in to join the conversation.

No comments yet. Be the first to share your thoughts.