We will cover briefly:
- Sign In using the Magic Link
- CRUD operations using Postgres
- Uploading images using Storage
Intro to Supabase

Before diving into Supabase, let’s provide an introduction to what Supabase is.
Supabase is an open-source alternative to Firebase that provides a set of backend services, including authentication, database, and storage.
Supabase offers some features like
- Auth service: for user authentication.
- Database service: standard PostgreSQL.
- Realtime service: sending messages and states to clients.
- Edge functions: Running server-side TypeScript functions, distributed globally at the edge
- Storage service: Storing and serving files.

To integrate Supabase into a Flutter application, we’ll utilize the supabase_flutter
package. Initializing the Supabase client can be done using the following code snippet.
void main() async {
await Supabase.initialize(
url: SUPABASE_URL,
anonKey: SUPABASE_ANON_KEY,
);
}
final supabase = Supabase.instance.client;
Sign In using Magic Link
It allows users to sign in to your app using Supabase Auth with the convenience of a magic link.
- To enable the native app to open when a user clicks on a link, it is necessary to configure deep links. When utilizing Supabase auth, certain scenarios result in the app opening upon link click. To support these scenarios effectively, setting up deep links is essential.

When users enter their email addresses, an email containing a verification link is sent to them. Users can then click on the link to verify their email and gain access to the app. This method eliminates the need for traditional password-based authentication and provides a secure and user-friendly login experience.
// Magic link login
await supabase.auth.signInWithOtp(email: 'my_email@example.com');

If you do not receive the OTP, make sure to verify the following:
- Ensure that the email authentication provider is enabled within the
Authentication
Supabase settings.
After receiving the email, you should be able to sign into your app by clicking on it. To achieve this, you need to register a redirect URL inside Supabase.
- Go to the Supabase project Authentication Settings page.
- Enter your app redirect callback on
Additional Redirect URLs
field.
The redirect callback url should have the format
[YOUR_SCHEME]://[YOUR_HOSTNAME]
- Follow the steps for Android and iOS

Once you successfully enter the app, your profile information is stored within the authenticated user’s data.

- When the user comes back to the app, we verify if there is a current session with the Supabase client.
- If there is an active session (i.e., the user is already authenticated), we redirect the user to the home screen
- If there is no active session (i.e., the user is not authenticated or the session has expired), we redirect the user to the login screen to initiate the login process.
void redirectAfterAuth() {
final session = _supabaseService.supabaseClient.auth.currentSession;
if (session != null) {
_navigationService.pushNamedAndRemoveUntil(
AppRoutes.homeRoute,
(_) => false,
);
} else {
_navigationService.pushNamedAndRemoveUntil(
AppRoutes.loginRoute,
(_) => false,
);
}
}
AddOns:
You have the option to personalize the email messages utilized during authentication processes. You are able to modify the content of the following email templates:
- Email confirmation during signup
- User invitation
- Magic Link authentication
- Changing email address
- Password reset notification
Listen to Auth Changes
We listen to the changes in supabase authentication using the auth.onAuthStateChange
method. This allows us to receive notifications whenever an authentication event occurs. We receive a AuthState
which includes
class AuthState {
final AuthChangeEvent event;
final Session? session;
AuthState(this.event, this.session);
}
We extract the session data and store it within our application’s global information. Upon the user’s app launch, we verify the presence of the session and guide the redirection process accordingly.
final session = _supabaseService.supabaseClient.auth.currentSession;
if (session != null) {
// Go to home
} else {
// Go to login
}
The session class comprises the following params
class Session {
final String? providerToken;
final String? providerRefreshToken;
final String accessToken;
final int? expiresIn;
final String? refreshToken;
final String tokenType;
final User user;
}
Store data using Postgres
User profiles and logged mood entries are securely stored and retrieved using the Supabase database. The PostgreSQL database allows for efficient and reliable data storage, ensuring that user information and mood records are safely managed.

We create two tables inside Postgres:
mood_tracker
: For storing the user’s moods informationprofiles
: For storing the user’s profile information

- Upon reaching the home screen, users have the option to select their mood by clicking on the emoji buttons. Subsequently, the chosen mood will be stored in the
mood_tracker
table in the Postgres database.
Future<bool> updateMood(MoodEntry mood) async {
try {
await supabase!.from('mood_tracker').upsert(mood.toJson());
return true;
} catch (error) {
debugPrint('Unexpected error occurred ${error.toString()}');
}
return false;
}
MoodEntry
is a model class that consists of the following params:
class MoodEntry {
final String moodName;
final String? updatedAt;
final String? data;
final String? id;
final String userId;
final String date;
}
- We utilize the Supabase client, previously established in the initial step, to invoke the
upsert
method. We pass the model as JSON, and we perform the necessary data insertion or update in the database.
When performing an upsert, the database system first attempts to find a matching record based on certain criteria, such as a unique key or primary key. If a matching record is found, the operation updates the existing record with the provided data. If no matching record is found, a new record is inserted with the provided data.
This is how the data gets stored inside the table

Upon navigating to the calendar screen, when the user chooses a specific date, we retrieve the corresponding mood recorded for that particular day.
Future<List<MoodEntry>> fetchMoodRecords(String dateFrom, String dateTo) async {
try {
final data = await supabase!
.from('mood_tracker')
.select()
.gte(ColumnResource.moodTrackerUpdatedAt, dateFrom)
.lte(ColumnResource.moodTrackerUpdatedAt, dateTo);
final modelResp = convertToModelList(data);
return modelResp;
} catch (error) {
debugPrint('Unexpected error occurred ${error.toString()}');
}
return [];
}
- We use the
.from()
method to specify the table to query. The.select()
method indicates that all columns should be selected. - The
.gte()
method filters the data to include only rows where the value in themoodTrackerUpdatedAt
column is greater than or equal to thedateFrom
value. - Similarly, the
.lte()
method filters the data to include only rows where the value in themoodTrackerUpdatedAt
column is less than or equal to thedateTo
value.

Getting the Profile
When the users click on the gear icon button, we show them the Profile Screen
We query the profiles
table and the fetched data is then structured into a UserModel
format.

final userId = supabase?.auth.currentUser!.id;
final data = await supabase!
.from('profiles')
.select()
.eq('id', userId)
.single() as Map<String, dynamic>;
final profile = UserProfile.fromJson(data);
return profile;
.select()
: This specifies that a SELECT operation is being performed on theprofiles
table..eq('id', userId)
: This adds a filter condition to the query. It specifies that only rows where theid
column is equal to the value ofuserId
should be selected..single()
: This indicates that you expect only a single result to be returned from the query.
Note: The
currentUser
property represents the currently authenticated user and theid
property of the user object retrieves their unique identifier
The data returned is then converted to UserProfile
(a model class)
class UserProfile {
String id;
String? updatedAt;
String? username;
String? avatarUrl;
String? website;
}
In addition, users have the option to modify their profile details by accessing the profile screen. Upon making changes to their name, they can save the updates by clicking the Update
button. Internally, we call the upsert
operation (as mentioned above) for saving changes to the database.

Uploading images using Storage
User-uploaded profile images are securely stored in Supabase storage. The generated URLs are then retrieved within the application to display user avatars, providing a seamless and efficient way to manage user profile images.
We create a avatar
bucket inside our project’s storage. Buckets are distinct containers for files and folders.

The client app should support picking image files, from the local file explorer and sending over them to the Supabase Storage. Choosing files from the system is done through image_picker
Once the user clicks on the Upload Photo
button, we call the ImagePicker.pickImage
final imageFile = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 300,
maxHeight: 300,
);
- The
source
parameter is setImageSource.gallery
which specifies that the image should be picked from the device’s gallery. - The
maxWidth
andmaxHeight
parameters limit the size of the picked image.
Next, we get the bytes from the image by calling
final bytes = await imageFile.readAsBytes();
Finally, we call the uploadBinary
function from Supabase storage.
final imageUrl = await _supabaseService.uploadBinary(
fileDetails.fileName,
fileDetails.bytes,
fileDetails.imageFile,
);
Once the file gets uploaded, we can create a signedUrl
to download file without requiring permission. This URL can be valid for a set number of seconds. In our case, we set it to 1 year
final imageUrlResponse = await supabase!.storage
.from('avatars')
.createSignedUrl(filePath, 60 * 60 * 24 * 365 * 10);
Bonus: App SignOut
We call the signOut
function from the Supabase’s auth
for signing out the current user, if there is a logged-in user.
await supabase?.auth.signOut();