Storage
Managing file uploads, downloads, and media storage with Supabase Storage buckets.
Supabase Storage provides a scalable file storage solution for your Flutter apps. It's built on top of Amazon S3 and offers:
- Public & Private Buckets – Control who can access your files
- Image Optimization – Automatic resizing and optimization
- CDN Delivery – Fast global content delivery
- File Management – Upload, download, delete, and list files
- Security – Row Level Security policies for storage
✅ Storage Use Cases
- Profile Pictures – User avatars uploaded to a public bucket
- Post Images – Images for posts or articles
- Video Uploads – User-generated video content
- Documents – PDFs, spreadsheets, and other documents
- Chat Media – Images and files in chat messages
To use storage in your Flutter app, you need to create a bucket and set up the proper policies.
-- Create a public bucket for avatars
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
-- Create a private bucket for user documents
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false);
-- Policy: Allow users to upload their own avatar
CREATE POLICY "Users can upload their own avatar"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
-- Policy: Allow users to read any avatar
CREATE POLICY "Anyone can read avatars"
ON storage.objects
FOR SELECT
USING (bucket_id = 'avatars');
Uploading files to Supabase Storage is done through the
storage
API.
import
'dart:io'
;
import
'package:supabase_flutter/supabase_flutter.dart'
;
import
'package:image_picker/image_picker.dart'
;
class
StorageService
{
final
SupabaseClient client = Supabase.instance.client;
// Upload an image file
Future
<String?>
uploadAvatar
({
required
String userId,
required
XFile file,
}) async {
try
{
// Convert XFile to File
final
File imageFile = File(file.path);
final
bytes =
await
imageFile.readAsBytes();
// Generate a unique file name
final
timestamp = DateTime.now().millisecondsSinceEpoch;
final
filePath =
'$userId/avatar_$timestamp.jpg'
;
// Upload to the avatars bucket
final
response =
await
client.storage
.from(
'avatars'
)
.uploadBinary(
filePath,
bytes,
fileOptions: FileOptions(
cacheControl:
'3600'
,
upsert:
true
,
contentType:
'image/jpeg'
,
),
);
// Get the public URL
final
publicUrl = client.storage
.from(
'avatars'
)
.getPublicUrl(filePath);
return
publicUrl;
}
catch
(e) {
print(
'Upload failed: $e'
);
return
null
;
}
}
// Upload a post image
Future
<String?>
uploadPostImage
({
required
String postId,
required
XFile file,
}) async {
try
{
final
File imageFile = File(file.path);
final
bytes =
await
imageFile.readAsBytes();
final
timestamp = DateTime.now().millisecondsSinceEpoch;
final
filePath =
'posts/$postId/image_$timestamp.jpg'
;
final
response =
await
client.storage
.from(
'posts'
)
.uploadBinary(filePath, bytes);
final
publicUrl = client.storage
.from(
'posts'
)
.getPublicUrl(filePath);
return
publicUrl;
}
catch
(e) {
print(
'Upload failed: $e'
);
return
null
;
}
}
}
class
ProfileScreen
extends
StatefulWidget {
@override
State<ProfileScreen>
createState
() => _ProfileScreenState();
}
class
_ProfileScreenState
extends
State<ProfileScreen> {
final
StorageService _storageService = StorageService();
final
ImagePicker _picker = ImagePicker();
String? _avatarUrl;
bool
_isUploading =
false
;
Future
<
void
>
_pickImage
() async {
final
XFile? image =
await
_picker.pickImage(
source: ImageSource.gallery,
maxWidth:
512
,
maxHeight:
512
,
imageQuality:
80
,
);
if
(image !=
null
) {
await
_uploadImage(image);
}
}
Future
<
void
>
_uploadImage
(XFile image) async {
setState(() => _isUploading =
true
);
final
user = Supabase.instance.client.auth.currentUser;
if
(user ==
null
)
return
;
final
url =
await
_storageService.uploadAvatar(
userId: user.id,
file: image,
);
if
(url !=
null
) {
setState(() {
_avatarUrl = url;
_isUploading =
false
;
});
}
else
{
setState(() => _isUploading =
false
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Upload failed'
),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(
title: Text(
'Profile'
),
centerTitle:
true
,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Avatar
Stack(
children: [
CircleAvatar(
radius:
50
,
backgroundColor: Colors.grey.shade200,
backgroundImage: _avatarUrl !=
null
? NetworkImage(_avatarUrl!)
:
null
,
child: _avatarUrl ==
null
? Icon(Icons.person, size:
50
, color: Colors.grey.shade400)
:
null
,
),
if
(_isUploading)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(
0.5
),
shape: BoxShape.circle,
),
child: Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
),
Positioned(
bottom:
0
,
right:
0
,
child: CircleAvatar(
radius:
16
,
backgroundColor: Colors.deepPurple,
child: IconButton(
icon: Icon(Icons.camera_alt, size:
16
, color: Colors.white),
padding: EdgeInsets.zero,
onPressed: _pickImage,
),
),
),
],
),
SizedBox(height:
16
),
Text(
'Tap the camera icon to upload a profile picture'
,
style: TextStyle(color: Colors.grey.shade600),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
There are two ways to access files from Supabase Storage: public URLs and signed URLs.
class
DownloadService
{
final
SupabaseClient client = Supabase.instance.client;
// Get public URL (for public buckets)
String
getPublicUrl
(String bucket, String path) {
return
client.storage.from(bucket).getPublicUrl(path);
}
// Get signed URL (for private buckets, expires after 60 seconds)
Future
<String?>
getSignedUrl
({
required
String bucket,
required
String path,
int
expiresIn =
60
,
}) async {
try
{
final
response =
await
client.storage
.from(bucket)
.createSignedUrl(path, expiresIn);
return
response;
}
catch
(e) {
print(
'Error getting signed URL: $e'
);
return
null
;
}
}
// Download file as bytes
Future
<List<int>?>
downloadFile
({
required
String bucket,
required
String path,
}) async {
try
{
final
response =
await
client.storage
.from(bucket)
.download(path);
return
response;
}
catch
(e) {
print(
'Error downloading file: $e'
);
return
null
;
}
}
// Get file information
Future
<Map<String, dynamic>?>
getFileInfo
({
required
String bucket,
required
String path,
}) async {
try
{
final
response =
await
client.storage
.from(bucket)
.info(path);
return
response;
}
catch
(e) {
print(
'Error getting file info: $e'
);
return
null
;
}
}
}
🔑 Public vs Signed URLs
- Public URLs – Anyone can access. Use for public buckets (avatars, logos, etc.)
- Signed URLs – Temporary access. Use for private buckets (user documents, etc.)
- Signed URLs expire – Default is 60 seconds, can be customized
You can list, move, copy, and delete files in Supabase Storage.
class
StorageManager
{
final
SupabaseClient client = Supabase.instance.client;
// List files in a folder
Future
<List<Map<String, dynamic>>>
listFiles
({
required
String bucket,
String? path,
int
? limit,
String? offset,
}) async {
try
{
final
response =
await
client.storage
.from(bucket)
.list(path ??
''
, options: ListOptions(
limit: limit,
offset: offset,
sortBy:
'created_at'
,
));
return
response;
}
catch
(e) {
print(
'Error listing files: $e'
);
return
[];
}
}
// Move a file
Future
<
bool
>
moveFile
({
required
String bucket,
required
String fromPath,
required
String toPath,
}) async {
try
{
await
client.storage
.from(bucket)
.move(fromPath, toPath);
return
true
;
}
catch
(e) {
print(
'Error moving file: $e'
);
return
false
;
}
}
// Copy a file
Future
<
bool
>
copyFile
({
required
String bucket,
required
String fromPath,
required
String toPath,
}) async {
try
{
await
client.storage
.from(bucket)
.copy(fromPath, toPath);
return
true
;
}
catch
(e) {
print(
'Error copying file: $e'
);
return
false
;
}
}
// Delete a file
Future
<
bool
>
deleteFile
({
required
String bucket,
required
String path,
}) async {
try
{
await
client.storage
.from(bucket)
.remove([path]);
return
true
;
}
catch
(e) {
print(
'Error deleting file: $e'
);
return
false
;
}
}
// Delete multiple files
Future
<
bool
>
deleteMultipleFiles
({
required
String bucket,
required
List<String> paths,
}) async {
try
{
await
client.storage
.from(bucket)
.remove(paths);
return
true
;
}
catch
(e) {
print(
'Error deleting files: $e'
);
return
false
;
}
}
}
Here's a complete image gallery app using Supabase Storage.
import
'dart:io'
;
import
'package:flutter/material.dart'
;
import
'package:supabase_flutter/supabase_flutter.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:cached_network_image/cached_network_image.dart'
;
void
main
() async {
await
Supabase.initialize(
url:
'https://your-project.supabase.co'
,
anonKey:
'your-anon-key'
,
);
runApp(
MyApp
());
}
class
MyApp
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
title:
'Image Gallery'
,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3:
true
,
),
home: GalleryScreen(),
);
}
}
// ==========================================
// 1. STORAGE SERVICE
// ==========================================
class
GalleryStorageService
{
final
SupabaseClient client = Supabase.instance.client;
Future
<String?>
uploadImage
({
required
XFile file,
required
String userId,
}) async {
try
{
final
bytes =
await
File(file.path).readAsBytes();
final
timestamp = DateTime.now().millisecondsSinceEpoch;
final
path =
'$userId/gallery_$timestamp.jpg'
;
await
client.storage
.from(
'gallery'
)
.uploadBinary(path, bytes);
return
client.storage
.from(
'gallery'
)
.getPublicUrl(path);
}
catch
(e) {
print(
'Upload failed: $e'
);
return
null
;
}
}
Future
<List<Map<String, dynamic>>>
listImages
(String userId) async {
try
{
return
await
client.storage
.from(
'gallery'
)
.list(userId);
}
catch
(e) {
print(
'List failed: $e'
);
return
[];
}
}
Future
<
bool
>
deleteImage
(String path) async {
try
{
await
client.storage
.from(
'gallery'
)
.remove([path]);
return
true
;
}
catch
(e) {
print(
'Delete failed: $e'
);
return
false
;
}
}
String
getPublicUrl
(String path) {
return
client.storage.from(
'gallery'
).getPublicUrl(path);
}
}
// ==========================================
// 2. GALLERY SCREEN
// ==========================================
class
GalleryScreen
extends
StatefulWidget {
@override
State<GalleryScreen>
createState
() => _GalleryScreenState();
}
class
_GalleryScreenState
extends
State<GalleryScreen> {
final
GalleryStorageService _storageService = GalleryStorageService();
final
ImagePicker _picker = ImagePicker();
List<Map<String, dynamic>> _images = [];
bool
_isLoading =
true
;
bool
_isUploading =
false
;
String? _userId;
@override
void
initState
() {
super
.initState();
_userId = Supabase.instance.client.auth.currentSession?.user.id;
if
(_userId !=
null
) {
_loadImages();
}
}
Future
<
void
>
_loadImages
() async {
setState(() => _isLoading =
true
);
final
images =
await
_storageService.listImages(_userId!);
setState(() {
_images = images;
_isLoading =
false
;
});
}
Future
<
void
>
_pickAndUpload
() async {
final
XFile? image =
await
_picker.pickImage(
source: ImageSource.gallery,
maxWidth:
1024
,
maxHeight:
1024
,
imageQuality:
80
,
);
if
(image ==
null
|| _userId ==
null
)
return
;
setState(() => _isUploading =
true
);
final
url =
await
_storageService.uploadImage(
file: image,
userId: _userId!,
);
setState(() => _isUploading =
false
);
if
(url !=
null
) {
await
_loadImages();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Image uploaded!'
), backgroundColor: Colors.green),
);
}
else
{
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Upload failed'
), backgroundColor: Colors.red),
);
}
}
Future
<
void
>
_deleteImage
(String path) async {
setState(() => _isLoading =
true
);
final
success =
await
_storageService.deleteImage(path);
setState(() => _isLoading =
false
);
if
(success) {
await
_loadImages();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(
'Image deleted'
)),
);
}
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(
title: Text(
'🖼️ Gallery'
),
centerTitle:
true
,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadImages,
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _images.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.photo_library, size:
80
, color: Colors.grey.shade400),
SizedBox(height:
16
),
Text(
'No images yet'
,
style: TextStyle(fontSize:
20
, fontWeight: FontWeight.w600),
),
Text(
'Tap the + button to upload one'
,
style: TextStyle(color: Colors.grey.shade400),
),
],
),
)
: GridView.builder(
padding: EdgeInsets.all(
8
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
3
,
crossAxisSpacing:
4
,
mainAxisSpacing:
4
,
),
itemCount: _images.length,
itemBuilder: (context, index) {
final
image = _images[index];
final
path = image[
'name'
]
as
String;
final
fullPath =
'$_userId/$path'
;
final
url = _storageService.getPublicUrl(fullPath);
return
Stack(
children: [
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
8
),
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
height:
300
,
child: Center(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
height:
300
,
color: Colors.grey.shade200,
child: Icon(Icons.error, color: Colors.red),
),
),
),
Padding(
padding: EdgeInsets.all(
8
),
child: ElevatedButton.icon(
icon: Icon(Icons.delete, color: Colors.red),
label: Text(
'Delete'
),
onPressed: () {
Navigator.pop(context);
_deleteImage(fullPath);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade50,
foregroundColor: Colors.red,
),
),
),
],
),
),
);
},
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(
4
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
4
),
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
placeholder: (context, url) => Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Icon(
Icons.broken_image,
color: Colors.grey.shade400,
),
),
),
),
),
],
);
},
),
floatingActionButton: _isUploading
? FloatingActionButton(
onPressed:
null
,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth:
2
,
),
)
: FloatingActionButton(
onPressed: _pickAndUpload,
child: Icon(Icons.add),
),
);
}
}
storage.from(bucket).uploadBinary()
to upload files.
getPublicUrl()
for public buckets or
createSignedUrl()
for private buckets.
CachedNetworkImage
for efficient image loading with caching.
Without proper policies, users won't be able to upload or access files, even if the bucket is public.
Create policies for INSERT, SELECT, DELETE operations on the
storage.objects
table.
Public URLs only work for public buckets. For private buckets, you need signed URLs.
Use
createSignedUrl()
to get temporary access to private files.
Uploads can fail for many reasons (network issues, file size limits, etc.). Always handle errors gracefully.
Use try-catch blocks and show user-friendly error messages when uploads fail.
🎯 Key Takeaway
Supabase Storage provides scalable file storage with public and private buckets. Use public URLs for public assets and signed URLs for private files. Always set up RLS policies for security and use CachedNetworkImage for efficient image display. Storage makes media management simple and secure.