Firebase Storage Integration

📺 Related video guides:

Note - this is based on a single preprogrammed user submitting file uploads via configurations in the .env file. This is NOT a multi user setup. You will need to change the security variables to allow for approved user groups.

This is also just AI’s interpretation of a working firebase storage use case that I have pulled out of my code base. Use it at your own caution, double check your work.

Prerequisites

  • A Firebase project
  • Next.js project with TypeScript
  • Basic understanding of TypeScript
  • Node.js and npm installed (if running locally)
  • Tailwind CSS configured in your project

Installation

Install the required dependencies:

npm install firebase uuid react-dropzone @radix-ui/react-slot class-variance-authority clsx tailwind-merge lucide-react

Environment Setup

Create a .env.local file with your Firebase configuration:

NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_auth_domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_storage_bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=your_measurement_id

Firebase Configuration

Create a lib/firebase.ts file to initialize Firebase:

import { initializeApp, getApps } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
};

// Initialize Firebase only once
let app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig);
let db = getFirestore(app);
let storage = getStorage(app);

export { app, db, storage };

Firebase Storage Rules

In your Firebase Console, navigate to Storage > Rules and set up appropriate security rules:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /screenshots/{filename} {
      allow read: if true;
      allow write: if 
        // Limit file size to 5MB
        request.resource.size < 5 * 1024 * 1024 &&
        // Only allow image files
        request.resource.contentType.matches('image/.*');
    }
    
    // Default deny for everything else
    match /{allPaths=**} {
      allow read, write: if false;
    }
  }
}

Form Schema

Create a validation schema for your form:

const formSchema = z.object({
  // ... other form fields ...
  screenshot: z.custom<FileList>()
    .refine((files) => files?.length === 1, "Screenshot is required")
    .refine(
      (files) => files?.[0]?.size <= MAX_FILE_SIZE,
      "Max file size is 5MB"
    )
    .refine(
      (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type),
      "Only .jpg, .jpeg, .png and .webp formats are supported"
    ),
});

File Upload Implementation

Create a function to handle file uploads:

import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import { v4 as uuidv4 } from 'uuid';
import { storage } from './firebase';

export async function submitProject(data: ProjectSubmission): Promise<{ success: boolean; id: string }> {
  try {
    // 1. Upload screenshot to Firebase Storage
    const file = data.screenshot[0];
    const fileExt = file.name.split('.').pop();
    const fileName = `${uuidv4()}.${fileExt}`;
    const storageRef = ref(storage, `screenshots/${fileName}`);
    
    const uploadResult = await uploadBytes(storageRef, file);
    const imageUrl = await getDownloadURL(storageRef);

    // 2. Save to database or perform other operations with the URL
    // ... your database operations here ...

    return { success: true, id: 'some-id' };
  } catch (error) {
    console.error('Error in submitProject:', error);
    throw error;
  }
}

Component Implementation

Here’s the implementation of the FileUpload component:

import React from 'react';
import { useDropzone, type DropzoneOptions, type FileRejection } from "react-dropzone";
import { cn } from "@/lib/utils";
import { Upload, X } from "lucide-react";
import { Button } from "./button";

interface FileUploadProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
  accept?: string;
  maxSize?: number;
  onValueChange?: (files: File[]) => void;
  recommendedSize?: string;
}

const FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(
  ({ className, accept, maxSize, onValueChange, recommendedSize, ...props }, ref) => {
    const [files, setFiles] = React.useState<File[]>([]);
    const [error, setError] = React.useState("");

    const dropzoneOptions: DropzoneOptions = {
      accept: accept ? { [accept]: [] } : undefined,
      maxSize,
      maxFiles: 1,
      noClick: true,
      onDrop: (acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
        if (rejectedFiles.length > 0) {
          const rejection = rejectedFiles[0];
          if (rejection.errors[0]?.code === "file-too-large") {
            setError(`File is too large. Max size is ${maxSize! / (1024 * 1024)}MB`);
          } else if (rejection.errors[0]?.code === "file-invalid-type") {
            setError("Invalid file type");
          } else {
            setError("Error uploading file");
          }
          return;
        }

        try {
          setFiles(acceptedFiles);
          setError("");
          if (onValueChange) {
            const dataTransfer = new DataTransfer();
            acceptedFiles.forEach((file: File) => dataTransfer.items.add(file));
            onValueChange(acceptedFiles);
          }
        } catch (err) {
          console.error("File handling error:", err);
          setError("Error processing file");
        }
      },
    };
    // ... rest of the component implementation
  }
);

Stackblitz Considerations

When running in Stackblitz Web Container, you need to handle CORS restrictions:

Firebase Storage CORS Configuration

  1. Go to Firebase Console > Storage
  2. Find your storage bucket settings
  3. Add CORS configuration for your domain:
[
  {
    "origin": ["https://*.stackblitz.io"],
    "method": ["GET", "POST", "PUT", "DELETE", "HEAD"],
    "maxAgeSeconds": 3600,
    "responseHeader": ["Content-Type"]
  }
]

Error Handling

Add specific error handling for Stackblitz environment:

try {
  await uploadBytes(storageRef, file);
} catch (error) {
  if (error.code === 'storage/unauthorized') {
    console.error('CORS or Firebase Storage rules may be blocking the upload');
    // Handle appropriately
  }
  throw error;
}

Best Practices

File Type Validation

const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];

Progress Tracking

const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on('state_changed',
  (snapshot) => {
    const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    console.log('Upload is ' + progress + '% done');
  }
);

Error Messages

const getErrorMessage = (error: any) => {
  switch (error.code) {
    case 'storage/unauthorized':
      return 'User does not have permission to access the object';
    case 'storage/canceled':
      return 'User canceled the upload';
    case 'storage/unknown':
      return 'Unknown error occurred, inspect error.serverResponse';
    default:
      return 'An error occurred during upload';
  }
};

Testing

  1. Test file uploads with various file types and sizes
  2. Verify CORS configuration works in Stackblitz
  3. Test error scenarios and validation
  4. Verify uploaded files are accessible via their URLs

Troubleshooting

Common issues and solutions:

CORS Errors

  • Verify Firebase Storage CORS configuration
  • Check Firebase Storage rules
  • Ensure proper domain configuration

Upload Failures

  • Check file size limits
  • Verify Firebase configuration
  • Check network connectivity
  • Verify authentication state if required

Stackblitz-Specific Issues

  • Use the browser console to check for specific error messages
  • Verify environment variables are properly set
  • Check memory usage in the Stackblitz environment

Remember to handle cleanup of unused files and implement proper error handling for production environments.