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.