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
- Go to Firebase Console > Storage
- Find your storage bucket settings
- 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
- Test file uploads with various file types and sizes
- Verify CORS configuration works in Stackblitz
- Test error scenarios and validation
- 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.