Supabase private bucket + signed URLs in FlutterFlow — full working solution

Feedback

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

  1. Pick image → call uploadUserPhoto → save returned path to users.photo

  2. Anywhere you display the photo → call getPhotoUrl(user.photo, 'users') → bind to Image widget

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 🙏

3