Hey everyone,
Sharing a complete pattern for private profile photos with Supabase Storage in FlutterFlow. No Edge Function, no public bucket — just two Custom Actions.
Why not use FF's native upload?
The built-in action stores a public URL. If your bucket is private (RLS enabled), that URL is immediately broken. You also can't control the file path, which makes per-user access policies impossible.
Custom Action 1 — uploadUserPhoto
Uploads to a private bucket using the path userId/timestamp.ext, returns the path only (not a URL).
dart
Future<String> uploadUserPhoto(
FFUploadedFile uploadedFile,
String userId,
) async {
final supabase = Supabase.instance.client;
final bucket = supabase.storage.from('users');
final timestamp = DateTime.now().millisecondsSinceEpoch;
final ext = (uploadedFile.name?.split('.').last ?? 'jpg').toLowerCase();
final storagePath = '$userId/$timestamp.$ext';
await bucket.uploadBinary(
storagePath,
uploadedFile.bytes!,
fileOptions: FileOptions(contentType: 'image/$ext', upsert: false),
);
return storagePath; // store THIS in your users table, not a URL
}Custom Action 2 — getPhotoUrl
Generates a signed URL (6h validity) from the stored path.
dart
Future<String> getPhotoUrl(
String photoValue,
String bucket,
) async {
if (photoValue.isEmpty) return 'https://yourapp.com/default.png';
if (photoValue.startsWith('http')) return photoValue; // social login avatars
return await Supabase.instance.client.storage
.from(bucket)
.createSignedUrl(photoValue, 21600);
}Supabase RLS policy (storage.objects)
sql
-- Users can only access their own folder
create policy "User accesses own photos"
on storage.objects for all
using (
bucket_id = 'users'
and auth.uid()::text = (string_to_array(name, '/'))[1]
);Flow in FlutterFlow
Pick image → call
uploadUserPhoto→ save returned path tousers.photoAnywhere you display the photo → call
getPhotoUrl(user.photo, 'users')→ bind toImagewidget
That's it. Works with social login avatars too (the startsWith('http') guard handles them).
Hope this saves someone a few hours 🙏
@FlutterFlow — native private bucket support + signed URLs for user profiles would be great. Note that signed URLs expire (6h here), so the app needs to reload them on each session — all the more reason to have this handled natively 🙏