Firebase Integration

Cloud Storage — Security Rules & File Management

16 min Lesson 9 of 13

Cloud Storage — Security Rules & File Management

Firebase Cloud Storage lets you store and serve user-generated content such as images, videos, and documents at scale. But raw storage access is dangerous: without proper rules, anyone could read or overwrite every file in your bucket. In this lesson you will master Firebase Storage Security Rules, learn how to list bucket contents, delete files, and handle the errors that arise when rules block an operation.

Understanding Storage Security Rules

Storage security rules are written in a rules language that lives inside the Firebase console (or a storage.rules file). Rules evaluate before any SDK operation reaches your bucket. Every match block maps to a path pattern, and every allow statement grants a specific method (read, write, create, update, delete, or list).

The two most important built-in variables inside a rules expression are:

  • request.auth — the Firebase Auth token of the caller (null if unauthenticated)
  • request.auth.uid — the unique user ID string
Note: Rules are not filters. If a list rule denies access to a path, the entire list call fails — Firebase does not silently skip forbidden files. Design your path structure so that each user's files live under their own UID segment.

Scoping Read and Write to Authenticated Users

The most common pattern restricts a user's files to a folder named after their UID. Below is a minimal but production-ready ruleset:

storage.rules — per-user folder access

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // Public read for shared assets (e.g., app icons, avatars)
    match /public/{allPaths=**} {
      allow read: if true;
      allow write: if false;
    }

    // Each authenticated user owns their own folder
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }

    // Admins can read everything (custom claim required)
    match /{allPaths=**} {
      allow read: if request.auth != null
                  && request.auth.token.admin == true;
    }
  }
}

Key points in the ruleset above:

  • rules_version = '2' is mandatory for wildcard {allPaths=**} to work correctly.
  • The users/{userId}/ pattern binds the UID segment as a variable, then the condition compares it to request.auth.uid.
  • allow read, write is shorthand for granting all sub-operations (get, list, create, update, delete).

Listing Bucket Contents in Flutter

To enumerate files under a prefix, call listAll() (returns every item at once) or list() (returns a page with a nextPageToken). Use list() for folders with many files.

Listing a user's uploaded images

import 'package:firebase_storage/firebase_storage.dart';

Future<List<Reference>> listUserImages(String uid) async {
  final storageRef = FirebaseStorage.instance
      .ref()
      .child('users/$uid/images');

  // listAll() fetches every item and prefix (sub-folder)
  final ListResult result = await storageRef.listAll();

  // result.items  — list of file References
  // result.prefixes — list of sub-folder References
  return result.items;
}

// Usage with download URLs
Future<void> showUserImages(String uid) async {
  final items = await listUserImages(uid);
  for (final ref in items) {
    final url = await ref.getDownloadURL();
    print('File: ${ref.name} — URL: $url');
  }
}
Tip: listAll() loads everything into memory in one shot. For production buckets with hundreds of files per user, prefer list(maxResults: 20) combined with pagination tokens to keep memory usage predictable.

Deleting Files

Deleting a file requires a Reference to the exact path. The operation returns a Future<void> that resolves when the file is removed from the bucket.

Deleting a single file and handling errors

import 'package:firebase_storage/firebase_storage.dart';

Future<void> deleteUserFile({
  required String uid,
  required String fileName,
}) async {
  final ref = FirebaseStorage.instance
      .ref()
      .child('users/$uid/images/$fileName');

  try {
    await ref.delete();
    print('Deleted: $fileName');
  } on FirebaseException catch (e) {
    switch (e.code) {
      case 'object-not-found':
        // File was already removed — treat as success
        print('File not found, skipping: $fileName');
        break;
      case 'unauthorized':
        // Security rules denied the delete
        print('Permission denied: ${e.message}');
        rethrow; // Let the UI layer show an error
      default:
        print('Storage error [${e.code}]: ${e.message}');
        rethrow;
    }
  }
}

Handling Storage Errors Gracefully

Every Storage SDK call can throw a FirebaseException. The e.code property contains a short slug that describes the failure. The most important codes are:

  • object-not-found — the path does not exist in the bucket.
  • unauthorized — the Security Rules expression evaluated to false.
  • canceled — the upload or download task was explicitly cancelled.
  • retry-limit-exceeded — too many consecutive retries; check connectivity.
  • quota-exceeded — your free-tier storage or download quota is exhausted.
Warning: Never swallow all FirebaseExceptions silently. Log the e.code and e.message at minimum, and distinguish between "not found" (safe to ignore) and "unauthorized" (must surface to the user or your error tracking system).

Combining Rules, Listing, and Deletion in a Repository

A clean Flutter architecture wraps all Storage interactions in a repository class. This keeps your widgets free of SDK details and makes unit testing straightforward by allowing you to mock the repository.

StorageRepository with list, delete, and error handling

class StorageRepository {
  final FirebaseStorage _storage;
  StorageRepository({FirebaseStorage? storage})
      : _storage = storage ?? FirebaseStorage.instance;

  /// Returns download URLs for all files in a user's folder.
  Future<List<String>> getUserFileUrls(String uid) async {
    try {
      final result = await _storage
          .ref('users/$uid/images')
          .listAll();
      return await Future.wait(
        result.items.map((ref) => ref.getDownloadURL()),
      );
    } on FirebaseException catch (e) {
      if (e.code == 'unauthorized') {
        throw Exception('Access denied. Please sign in.');
      }
      rethrow;
    }
  }

  /// Deletes a file by its storage path.
  Future<void> deleteFile(String path) async {
    try {
      await _storage.ref(path).delete();
    } on FirebaseException catch (e) {
      if (e.code == 'object-not-found') return; // idempotent
      rethrow;
    }
  }
}

Summary

In this lesson you learned how to write Firebase Storage Security Rules that scope access to authenticated users and specific UID-based paths. You used listAll() to enumerate bucket contents, called delete() to remove files, and handled the most common FirebaseException error codes correctly. In the next lesson you will integrate Storage uploads and deletions directly into a Flutter UI with real-time progress indicators.