Rush Apps

How to retrieve media from private Supabase buckets in Flutterflow

Table of Contents

Sometimes you’ll have to deal with sensitive information (User’s profile images, medical information,financial documents e.t.c). One of the ways to add some security is to make your buckets private

Difference between Private and Public buckets in Supabase

Public Bucket

  • File URL is static (so it can be written to database table directly and retrieved by Flutterflow Application)
  • Access to file can be restricted by Bucket’s RLS rules

Private bucket

  • File URL is dynamic (so even if it is written to database, it will become useless as soon as URL validity time expires)
  • Access to file can be restricted by Bucket’s RLS rules

So, basically, the only difference is that URL in Private bucket is not static. In private buckets File URL is formed when function below is called by your application

Source: Supabase Dart Client Documentation

				
					final String signedUrl = await supabase
  .storage
  .from('avatars')
  .createSignedUrl('folder_path/avatar1.png', 60);
				
			

What do we need to get SignedURL in Flutterflow?

Let’s come back to function above in order to understand what prerequisites we need to finally call it in Flutterflow

				
					final String signedUrl = await supabase
  .storage
  .from('avatars') // it is Bucket Name
  .createSignedUrl('folder_path/avatar1.png', 60); // it is Folder Path and validity time (in seconds)
				
			

So, in order to get User’s file signedURL, we’ll need:

1) Bucket Name
2) Path to file (and keep in mind that this Path should lead to User’s exclusive folder)
3) File name

Let’s think how we can get those!

Bucket Name can be set up manually (using Supabase GUI)

Folder Path. We need to somehow set up this string “folder_path/avatar1.png” to pass it to function. This can be set up as: “profile_pics/$UserID/$File Name”. “profile_pics/” we’ll write manually, while UserID we’ll get using FlutterFLow global parameters. However, those folders should somehow appear in our dedicated folder before we call this function. So, we’ll need to construct Custom Action in FlutterFLow to create User’s dedicated folder in Private Bucket

File Name. Imagine that we created a folder using  Custom Action above. We have one last thing missing: File Name. Unfortunately, using Flutterflow we cannot manipulate naming of Files that users upload using application. When uploaded it usually has such format: 

171214700000.jpeg

But, we can create Custom Action that will fetch “profile_pics/$UserID” folder and return name of last uploaded file (and this is the last thing that we need to set up path to file to invoke function to get file’s signedURL!)

 

As we now understand how to invoke function to get signedURL, let’s start writing Custom Actions and setting up necessary RLS rules in Supabase

 

Clone Flutterflow App to start

Feel free to clone this app. I’ve already set up all the Frontend that we’ll use further in this article:
https://app.flutterflow.io/project/private-bucket-retrieve-public-li54ap

1. Add your Supabase project

2. Enable Authentification (set “CreateAccount” as Entry Page
 and “Profile” as Logged In Page

3.Add Create Account action on page “CreateAccount” to button “Create Account”

Set Up your private bucket's RLS policies

1.Add Private Bucket called “Profile Pics” and create subfolder called “profile_pics”

2. Set up RLS. 

2.1 Click “Policies” under “Configuration” title

2.2 Click “New Policy”

2.3 Click “Get Started Quickly”

2.4 Select Option “Give users access to only their top level folder named as uid”. Then click “Use this Template”

3. Overwrite template code with the one below:

				
					((bucket_id = 'Profile Pics'::text) AND ((storage.foldername(name))[1] = 'profile_pics'::text) AND ((auth.uid())::text = (storage.foldername(name))[2]))
				
			

Explanation of code above:

      1.(storage.foldername(name))[1] = ‘profile_pics’::text

Here we look up for “first” level folder in bucket is calles “profile_pics” and give access to this folder

      2. (auth.uid())::text = (storage.foldername(name))[2]) 

Here we look up for “second” level folder in bucket that is called like UUID of user that is logged in and give access to this folder

Select “authenticated” target role and SELECT, INSERT, UPDATE, DELETE as allowed operation

Set Up Custom Action to create User's dedicated folder

Let’s dive into Flutterflow and create custom action to create User’s dedicated folder

1.Go to Custom Code section in FlutterFlow. Click “Add” button, select “Custom Action” .
2. Name this Custom Action “createUserFolder”
3. Toggle Return Value and choose String type. it should bu non-nullable and non-list



4. Copy and Paste code below:

				
					// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'dart:typed_data';
import 'package:flutter/foundation.dart' as found;

Future<String> createUserFolder() async {
  try {
    final folder_name_base = await SupaFlow.client.auth.currentUser?.id;

    // Stub for a file (empty binary)
    final Uint8List stubImage = Uint8List.fromList([0, 1, 0, 1, 1]); //

    // Use the stub binary file directly in storage client operations
    await SupaFlow.client.storage.from('Profile Pics').uploadBinary(
        'profile_pics/$folder_name_base/welcome_user.txt', stubImage);

    // Return success message if upload is successful
    return 'File uploaded successfully!';
  } catch (error_text) {
    // Return error message if any error occurs during upload
    return 'Error uploading file: $error_text';
  }
}

// Set your action name, define your arguments and return parameter,
// and then add the boilerplate code using the green button on the right!
				
			

Explanation of code above:

1) final folder_name_base = await SupaFlow.client.auth.currentUser?.id;

Here we retrieve authenticated user’s UUID and assign it to “folder_name_base” variable.
SupaFlow is just a wrapper for your connection to Supabase. Here’s the FlutterFlow’s source code for this class:

				
					class SupaFlow {
  SupaFlow._();

  static SupaFlow? _instance;
  static SupaFlow get instance => _instance ??= SupaFlow._();

  final _supabase = Supabase.instance.client;
  static SupabaseClient get client => instance._supabase;

  static Future initialize() => Supabase.initialize(
        url: _kSupabaseUrl,
        anonKey: _kSupabaseAnonKey,
        debug: false,
      );
}
				
			

2) final Uint8List stubImage =Uint8List.fromList([0,1,0,1,1]);

Unfortunately, supabase_flutter package that we use and that is accessible in FlutterFlow does not support creation of empty folders. However, we can add “empty” binary file to folder in bucket even if folder does not exist using uploadBinary() method from this package. This method creates folder in Bucket if it does not find folder matching the criteria set up by application’s function. So, in code above we initialize empty binary file and assign it to stubImage variable

3)

await SupaFlow.client.storage.from(‘Profile Pics’).uploadBinary(
        ‘profile_pics/$folder_name_base/welcome_user.txt’, stubImage);


Here we finally call uploadBinary method from supanase_flutter package and paste necessary variables:

 

3.1) from(‘Profile Pics’)
Here we paste Bucket’s name 

3.2) ‘profile_pics/$folder_name_base/welcome_user.txt’
Here we set up path to empty binary file that should be created (welcome_user.txt is a name of file to be created)

3.3) Lastly, we insert contents of our stub file using stubImage variable

Finally, do not forget to click “Save” and check Custom Action’s validity

Now, let’s dive into Frontend and assign this custom action to “CreateBucketAction” on our Profile Page

Choose  Custom Action and select the one that we just created|

Paste this into Action Output Variable Name

UserFolderActionResult

Trigger Animation

Test Results in Run Mode
Make sure that tapping container triggers the action we just created. Tap on container and go to Supabase Private Bucket that you just created. Result should look like below

Set Up Media Upload Action to upload profile Images to User's dedicated folder

Now, as we now have User’s dedicated folder in bucket, let’s set up an action to upload media there. This media we’ll try to automatically get further using getSignedURL method

1) Navigate to “Profile” page and select “Card” widget

2) Open Action Flow Editor and add “Upload Media to Supabase” action

 

3) Set Bucket name as “Profile Pics”
4) Set Upload Folder Path as follows:

5) Test this action in Run Mode and upload some media using this action. We’ll use this file in next action

Set Up Custom Action to receive name of last uploaded file in User's dedicated folder

Let’s create another Custom Action to get name of last uploaded file in User’s dedicated folder

1.Go to Custom Code section in FlutterFlow. Click “Add” button, select “Custom Action” .
2. Name this Custom Action “getLastUploadedFile”
3. Toggle Return Value and choose String type. it should be non-nullable and non-list

4. Copy and Paste code below:

				
					// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:storage_client/src/types.dart';

Future<String> getLastUploadedFile() async {
  try {
    final folder_name_base = await SupaFlow.client.auth.currentUser?.id;

    if (folder_name_base == null) {
      throw 'Error: User ID is null';
    }

    final searchOptions = SearchOptions(
      sortBy: SortBy(
          column: 'name',
          order: 'desc'), // Sort by name of file in descending order
    );

    final response = await SupaFlow.client.storage.from('Profile Pics').list(
        path: 'profile_pics/$folder_name_base/profile_image',
        searchOptions: searchOptions);

    if (response.isNotEmpty) {
      final files = response.first;
      // Extract URLs or other data from files if needed
      final fileURL = files.name;
      return fileURL;
    } else {
      return 'Listing files error';
    }
  } catch (error) {
    // Return null and log the error if any error occurs
    return 'Error in createSignedURL: $error';
  }
}
				
			

Now we are using storage_client api that is a part of supabase-flutter package. We’ll be using list() method in order to get last file that was uploaded to specific folder below is a code that defines this method. We’ll pass here “Path” to folder and define SerachOptions (we’ll use sortBy). You can take a look at code of list() method below:

				
					/// Lists all the files within a bucket.
  ///
  /// [path] The folder path.
  ///
  /// [searchOptions] includes `limit`, `offset`, and `sortBy`.
  Future<List<FileObject>> list({
    String? path,
    SearchOptions searchOptions = const SearchOptions(),
  }) async {
    final Map<String, dynamic> body = {
      'prefix': path ?? '',
      ...searchOptions.toMap(),
    };
    final options = FetchOptions(headers: headers);
    final response = await _storageFetch.post(
      '$url/object/list/$bucketId',
      body,
      options: options,
    );
    final fileObjects = List<FileObject>.from(
      (response as List).map(
        (item) => FileObject.fromJson(item),
      ),
    );
    return fileObjects;
  }
}
				
			

Please, take into account that here we’re going into another subfolder in User’s dedicated folder called “profile_image” that we create when uploading media using action from previous step. 

				
					

    final response = await SupaFlow.client.storage.from('Profile Pics').list(
        path: 'profile_pics/$folder_name_base/profile_image',
        searchOptions: searchOptions);

 
				
			

Finally, do not forget to click “Save” and check Custom Action’s validity

Now, let’s add this action to Frontend:

1) Select “GetProfileImageContainer” on “Profile” page

2) Add “getLastUploadFile” custom action to action flow editor


Paste it into Action Output Variable Name
LastProfileImageName

3) Trigger the animation

4) Add action output to text widget. This will help us to debug possible issues

Set Up Custom Action to getSignedUrl

Now we’re finally ready to set up Custom Action to get URL of image located in Private bucket

 

1.Go to Custom Code section in FlutterFlow. Click “Add” button, select “Custom Action” .
2. Name this Custom Action “getSignedURL”
3. Toggle Return Value and choose String type. it should be nullable and non-list
4. Add and argument called fileName set it to nullable and choose type “String”

4. Copy and Paste code below:

				
					// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

Future<String?> getSignedURL(String? fileName) async {
  try {
    final folder_name_base = await SupaFlow.client.auth.currentUser?.id;

    if (folder_name_base == null) {
      throw 'Error: User ID is null';
    }

    final signedUrl = await SupaFlow.client.storage
        .from('Profile Pics')
        .createSignedUrl(
            'profile_pics/$folder_name_base/profile_image/$fileName', 360);

    if (signedUrl.isNotEmpty) {
      return signedUrl;
    } else {
      return fileName;
    }
  } catch (error) {
    // Return null and log the error if any error occurs
    return 'Error in createSignedURL: $error';
  }
}

// Set your action name, define your arguments and return parameter,
// and then add the boilerplate code using the green button on the right!
				
			

Now we have everything that is needed for createSignedURL method:
1) profile/folder_name_base is set up by createUserFolderCustomAction
2) profile_image subfolder is being created when we upload media
3) fileName is retrieved using getLastUploadedFile method

Finally, do not forget to click “Save” and check Custom Action’s validity

Now, let’s add this action to Frontend:

1) Select “GetSignedURLContainer” on “Profile” page

 

2) Add “getSignedURL” custom action to action flow editor

 

2.1) set fileName argument to value that we receive when trigger getLastUploadedFile custom action

Paste it into Action Output Variable Name
SignedURLString

3) Trigger animation:

Let's finally test it!

1) Enter Run Mode

2)Click “Create Bicket Folder”. Make sure that User’s dedicated folder was created 

3)Click on avatar and upload any media of your choice

Make sure that it was uploaded to “profile_image” subfolder

4)Click on Get Profile Image and make sure that it shows name of your file

5)Click on Get Signed URL and make sure that it shows you the link

 

How to use this link to show media in the app?

Now as we know how to get this URL to image in private Bucket, we can use it in the app!

1) Set up page variable called imageLink. set the type to “ImagePath” and make it nullable

2) Go back to Custom Code section to getSignedURL Custom Action. Change Return Value type to ImagePath

3)Go to Profile page to Image widget

4) Set Image Path of this widget to conditional.
Condition should look at our page state variable and paste value of this variable if it is not null. If it is null, image should be taken from default location:


https://images.unsplash.com/photo-1592520113018-180c8bc831c9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTI3fHxwcm9maWxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=900&q=60


5) Modify Action Flow in GetSignedURLContainer and update our page state variable imageLink with action output of getSignedURL function

6)Remove getSIgnedURL action output in this text container

6)That’s it! Now you can test it. Click Get Signed URL button and see whether Profile image changed

How to use it in Production?

Of course, for demonstation purposes I triggered all the functions manually. But, for use in production you can use such workflow:

1) Trigger createUserFolderCustomAction when you create new user. In this case, whenever new user is being created, he’ll get dedicated folder in the bucket
2) Trigger 
getLastUploadedFile  and createSignedURL  on Page Load. In this case, if User uploaded media previously, it will be shown without manual trigger of function

Thank You

for reading this article. Hope, it will help in your FlutterFlow projects. If you’re stuck with specific question or you want us to help you create scalable and reliable web/mobile solution, feel free to fill “Contact Us” form. We’ll be happy to jump on a project of any difficulty and provide the best results possible