DEV Community

kanta13jp1
kanta13jp1

Posted on

Supabase Storage Guide: File Uploads, CDN, and Image Transforms

Supabase Storage Guide: File Uploads, CDN, and Image Transforms

Managing images, videos, and PDFs with Supabase Storage — S3-compatible, RLS-integrated, CDN-delivered.

How Supabase Storage Works

Supabase Storage structure:
  Bucket
    ├── public  → anyone can read
    └── private → RLS-controlled (auth required)

  Objects = files, managed by path
  Examples:
    avatars/<user_id>/profile.jpg
    posts/<post_id>/cover.webp
    documents/<user_id>/report.pdf
Enter fullscreen mode Exit fullscreen mode

Setup: Create Buckets via Migration

INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true)
ON CONFLICT DO NOTHING;

INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false)
ON CONFLICT DO NOTHING;

-- RLS: users can only access their own documents
CREATE POLICY "users can manage own documents"
ON storage.objects FOR ALL
TO authenticated
USING (
  bucket_id = 'documents'
  AND (storage.foldername(name))[1] = auth.uid()::text
);
Enter fullscreen mode Exit fullscreen mode

Upload from Flutter

import 'package:image_picker/image_picker.dart';

Future<String?> uploadAvatar(String userId) async {
  final picker = ImagePicker();
  final image = await picker.pickImage(
    source: ImageSource.gallery,
    maxWidth: 800,
    maxHeight: 800,
    imageQuality: 85,
  );
  if (image == null) return null;

  final bytes = await image.readAsBytes();
  final ext = image.path.split('.').last;
  final path = '$userId/profile.$ext';

  await supabase.storage
    .from('avatars')
    .uploadBinary(
      path,
      bytes,
      fileOptions: FileOptions(
        contentType: 'image/$ext',
        upsert: true,
      ),
    );

  return supabase.storage.from('avatars').getPublicUrl(path);
}
Enter fullscreen mode Exit fullscreen mode

CDN URLs and Image Transforms

// Public URL (served via CDN edge cache)
final publicUrl = supabase.storage
  .from('avatars')
  .getPublicUrl('user123/profile.jpg');

// Transform URL (resize + format convert)
final thumbnailUrl = supabase.storage
  .from('avatars')
  .getPublicUrl(
    'user123/profile.jpg',
    transform: TransformOptions(
      width: 100,
      height: 100,
      resize: ResizeOption.cover,
      format: FormatOption.webp,
      quality: 80,
    ),
  );
Enter fullscreen mode Exit fullscreen mode

Request the right size for each widget:

Widget buildAvatar(String userId, double size) {
  final px = (size * MediaQuery.of(context).devicePixelRatio).round();
  final url = supabase.storage
    .from('avatars')
    .getPublicUrl(
      '$userId/profile.jpg',
      transform: TransformOptions(width: px, height: px),
    );
  return CachedNetworkImage(imageUrl: url);
}
Enter fullscreen mode Exit fullscreen mode

Signed URLs for Private Files

// Temporary URL valid for 60 seconds
Future<String> getSignedUrl(String path) async {
  return supabase.storage.from('documents').createSignedUrl(path, 60);
}

// Download as bytes
Future<Uint8List> downloadFile(String path) async {
  return supabase.storage.from('documents').download(path);
}
Enter fullscreen mode Exit fullscreen mode

List and Delete Files

// List files in a folder
Future<List<FileObject>> listFiles(String userId) async {
  return supabase.storage.from('documents').list(path: userId);
}

// Delete
Future<void> deleteFile(String path) async {
  await supabase.storage.from('documents').remove([path]);
}
Enter fullscreen mode Exit fullscreen mode

Summary

Public bucket  → anyone can read (avatars, OGP images)
Private bucket → RLS + signed URLs (contracts, reports)
Transform API  → resize, WebP conversion via URL params
CDN            → automatic edge caching by Supabase
Enter fullscreen mode Exit fullscreen mode

Supabase Storage's key advantage: auth and RLS integrate seamlessly, so you don't need separate access control logic for file operations.

Top comments (0)