Skip to content

Instantly share code, notes, and snippets.

@imtipu
Last active November 28, 2025 07:05
Show Gist options
  • Select an option

  • Save imtipu/eeb1701d0fa6cae3940ac5ce67f42cb3 to your computer and use it in GitHub Desktop.

Select an option

Save imtipu/eeb1701d0fa6cae3940ac5ce67f42cb3 to your computer and use it in GitHub Desktop.
Use Firebase Storage as Django Media Storage

Django + Firebase Storage: A Practical Setup Guide

This guide walks you through integrating Firebase Storage with a Django project using the Firebase Admin SDK. It follows the exact configuration used in this codebase, so you can copy–paste safely.


What You’ll Build

  • A custom Django storage backend powered by Firebase Storage.
  • Environment-based toggle to switch between local file storage and Firebase Storage.
  • Signed URLs and public URLs for serving files securely.

Why Firebase Storage?

  • Global delivery via Google’s infrastructure with low‑ops overhead.
  • Simple Python SDK for uploads, signed URLs, and metadata.
  • Flexible security using service accounts and IAM.
  • Great for media (images, documents, avatars) with CDN‑friendly URLs.

Alternatives to consider: Amazon S3, Cloudflare R2, Supabase Storage, or the local filesystem for development.


Prerequisites

You can try other python and django versions.

  • Python 3.12+
  • Django 5.2+
  • A Firebase project with a Storage bucket
  • Service Account credentials for your Firebase project

Dependencies are already managed via pyproject.toml (includes firebase-admin and django-environ). If you prefer pip, see the commands below.


1) Install Dependencies

  • Using pip
pip install firebase-admin django-environ
  • Using uv
uv add firebase-admin django-environ

2) Prepare Firebase Service Account

  1. Go to Google Cloud Console → IAM & Admin → Service Accounts → Create key.
  2. Choose JSON and download the file.
  3. You’ll copy values from this JSON into environment variables (see below).

Important fields (names match your settings.py):

  • project_id
  • private_key_id
  • private_key (keep secure, replace literal newlines with \n in .env)
  • client_email
  • client_id
  • Your Firebase Storage bucket name (e.g., my-project.appspot.com)

3) Configure Environment Variables (.env)

Create or update your .env file at the project root:

USE_FIREBASE_STORAGE=True
FIREBASE_STORAGE_BUCKET=<bucket_name>
FIREBASE_PROJECT_ID=<project_id>
FIREBASE_PRIVATE_KEY_ID=<private_key_id>
FIREBASE_PRIVATE_KEY=<private_key_from_service_account>
FIREBASE_CLIENT_EMAIL=<client_email_from_service_account>
FIREBASE_CLIENT_ID=<client_id_from_service_account>

Notes:

  • If your private key contains actual newlines, replace them with \n in .env.
  • Set USE_FIREBASE_STORAGE=False locally if you prefer filesystem storage.

4) Update Django Settings

Your settings.py already includes the required configuration using django-environ and a conditional DEFAULT_FILE_STORAGE switch. Key parts:

# Base toggle and credentials
USE_FIREBASE_STORAGE = env.bool('USE_FIREBASE_STORAGE', default=False)
FIREBASE_STORAGE_BUCKET = env.str('FIREBASE_STORAGE_BUCKET', default='')
FIREBASE_PROJECT_ID = env.str('FIREBASE_PROJECT_ID', default='')
FIREBASE_PRIVATE_KEY_ID = env.str('FIREBASE_PRIVATE_KEY_ID', default='')
FIREBASE_PRIVATE_KEY = env.str('FIREBASE_PRIVATE_KEY', default='')
FIREBASE_CLIENT_EMAIL = env.str('FIREBASE_CLIENT_EMAIL', default='')
FIREBASE_CLIENT_ID = env.str('FIREBASE_CLIENT_ID', default='')

if USE_FIREBASE_STORAGE and FIREBASE_STORAGE_BUCKET:
    DEFAULT_FILE_STORAGE = 'app_storages.firebase_storage.FirebaseStorage'

STORAGES = {
    'default': {
        'BACKEND': DEFAULT_FILE_STORAGE
    },
}

This means when USE_FIREBASE_STORAGE=True and a bucket is provided, Django’s default storage becomes the custom Firebase backend.


5) Create the Firebase Storage Backend

Create app_storages/firebase_storage.py (already present in this repo). It defines a deconstructible class FirebaseStorage that:

  • Initializes the Firebase Admin SDK from env vars
  • Uploads files to your bucket
  • Generates signed URLs via generate_signed_url
  • Supports exists, delete, listdir, size, and time metadata helpers

Key behaviors to know:

  • _save makes uploaded files public by default using blob.make_public().
  • url(name) and signed_url(name) can return short-lived access URLs.
  • Helper firebase_upload_path(instance, filename) builds date-based paths: storage/<model>/<YYYY>/<MM>/<DD>/<filename>.

6) Use It in Your Models

If Firebase storage is the default, you can use ImageField or FileField as usual.

Example using the provided firebase_upload_path helper:

from django.db import models
from app_storages.firebase_storage import firebase_upload_path

class Profile(models.Model):
    avatar = models.ImageField(upload_to=firebase_upload_path, blank=True, null=True)

When USE_FIREBASE_STORAGE=True, files uploaded via admin, forms, or DRF will go to Firebase Storage automatically.


7) Programmatic Uploads (Views/Services)

from django.core.files.storage import default_storage

def save_bytes(path: str, data: bytes, content_type: str | None = None):
    # path example: "uploads/docs/readme.txt"
    name = default_storage.save(path, data)
    url = default_storage.url(name)
    return name, url

For signed access:

from app_storages.firebase_storage import get_firebase_storage

storage = get_firebase_storage()
signed = storage.signed_url('path/in/bucket.ext', expiration=3600)  # 1 hour

8) Local Development vs Production

  • Local development: set USE_FIREBASE_STORAGE=False to store files on the local filesystem (mediafiles/).
  • Production: set USE_FIREBASE_STORAGE=True with valid Firebase env vars.
  • Static files are still served by WhiteNoise in this project; Firebase is for user-uploaded media.

9) Security & Best Practices

  • Never commit raw service account JSON or secrets to version control.
  • Restrict your service account permissions to only what’s needed for Storage.
  • Prefer signed URLs over public blobs for private assets.
  • Rotate keys if they’re exposed.

10) Troubleshooting

  • Files not uploading

    • Check USE_FIREBASE_STORAGE and FIREBASE_STORAGE_BUCKET values.
    • Verify private key formatting. In .env, newlines must be \n.
    • Confirm the bucket exists and your service account has access.
  • Getting permission errors

    • Ensure the service account has Storage Object Admin on the bucket.
  • Wrong URLs or 404s

    • Ensure the blob path matches your upload_to logic.
    • For signed URLs, ensure system time is correct and expiration is in the future.

Author Profile Card

Author Photo

Tip: Replace the image path and links above with your actual assets. For a square avatar, use 256×256 or 512×512.


11) Useful References


12) Quick Checklist

  • Install firebase-admin and django-environ
  • Add .env with Firebase credentials and USE_FIREBASE_STORAGE=True
  • Ensure app_storages.FirebaseStorage is active via settings.py
  • Use upload_to=firebase_upload_path in your models (optional but recommended)
  • Deploy and test an upload (admin/DRF/form)

# {root_project}/app_storages/firebase_storage.py
import os
from datetime import datetime, timedelta, timezone
from django.core.files.storage import Storage
from django.core.files.base import ContentFile
from django.utils.deconstruct import deconstructible
import firebase_admin
from firebase_admin import credentials, storage
from django.conf import settings
@deconstructible
class FirebaseStorage(Storage):
"""
Custom storage backend for Firebase Storage using Firebase Admin SDK
"""
def __init__(self, bucket_name=None):
self.bucket_name = bucket_name or settings.FIREBASE_STORAGE_BUCKET
self._bucket = None
self._initialize_firebase()
def _initialize_firebase(self):
"""Initialize Firebase Admin SDK if not already initialized"""
if not firebase_admin._apps:
# Use environment variables for Firebase credentials
print('Initializing Firebase Admin SDK')
cred_dict = {
"type": "service_account",
"project_id": settings.FIREBASE_PROJECT_ID,
"private_key_id": settings.FIREBASE_PRIVATE_KEY_ID,
"private_key": settings.FIREBASE_PRIVATE_KEY.replace('\\n', '\n'),
"client_email": settings.FIREBASE_CLIENT_EMAIL,
"client_id": settings.FIREBASE_CLIENT_ID,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": f"https://www.googleapis.com/robot/v1/metadata/x509/{settings.FIREBASE_CLIENT_EMAIL}"
}
cred = credentials.Certificate(cred_dict)
firebase_admin.initialize_app(cred, {
'storageBucket': self.bucket_name
})
@property
def bucket(self):
"""Get Firebase storage bucket instance"""
if self._bucket is None:
self._bucket = storage.bucket()
return self._bucket
def _save(self, name, content):
"""Save file to Firebase Storage"""
blob = self.bucket.blob(name)
# Read content and upload
if hasattr(content, 'read'):
content.seek(0)
blob.upload_from_file(content, content_type=self._guess_content_type(name))
else:
blob.upload_from_string(content, content_type=self._guess_content_type(name))
# Make the blob publicly accessible
blob.make_public()
return name
def _open(self, name, mode='rb'):
"""Open file from Firebase Storage"""
blob = self.bucket.blob(name)
if not blob.exists():
raise FileNotFoundError(f"File {name} not found in Firebase Storage")
# Download as bytes
file_content = blob.download_as_bytes()
return ContentFile(file_content, name=name)
def delete(self, name):
"""Delete file from Firebase Storage"""
blob = self.bucket.blob(name)
if blob.exists():
blob.delete()
return None
def exists(self, name):
"""Check if file exists in Firebase Storage"""
blob = self.bucket.blob(name)
return blob.exists()
def listdir(self, path):
"""List directories and files in Firebase Storage"""
if path and not path.endswith('/'):
path += '/'
blobs = list(self.bucket.list_blobs(prefix=path, delimiter='/'))
directories = []
files = []
for blob in blobs:
if blob.name.endswith('/'):
# It's a directory
dir_name = blob.name[len(path):].rstrip('/')
if dir_name:
directories.append(dir_name)
else:
# It's a file
file_name = blob.name[len(path):]
if file_name and '/' not in file_name:
files.append(file_name)
return directories, files
def size(self, name):
"""Get file size"""
blob = self.bucket.blob(name)
if blob.exists():
blob.reload()
return blob.size
raise FileNotFoundError(f"File {name} not found")
def url(self, name, expiration=3600):
"""Get public URL for the file"""
blob = self.bucket.blob(name)
if blob.exists():
# Generate signed URL with expiration
expiration_time = datetime.now(timezone.utc) + timedelta(seconds=expiration)
return blob.generate_signed_url(expiration=expiration_time)
return None
def signed_url(self, name, expiration=3600):
"""Generate a signed URL for private access"""
blob = self.bucket.blob(name)
if blob.exists():
# Generate signed URL with expiration
expiration_time = datetime.now(timezone.utc) + timedelta(seconds=expiration)
return blob.generate_signed_url(expiration=expiration_time)
return None
def get_modified_time(self, name):
"""Get last modified time"""
blob = self.bucket.blob(name)
if blob.exists():
blob.reload()
return blob.updated
raise FileNotFoundError(f"File {name} not found")
def get_created_time(self, name):
"""Get creation time"""
blob = self.bucket.blob(name)
if blob.exists():
blob.reload()
return blob.time_created
raise FileNotFoundError(f"File {name} not found")
def get_accessed_time(self, name):
"""Get last accessed time (same as modified time for Firebase)"""
return self.get_modified_time(name)
def generate_signed_url(self, name, expiration=3600):
"""Generate a signed URL for private access"""
blob = self.bucket.blob(name)
if blob.exists():
# Generate signed URL with expiration
expiration_time = datetime.now(timezone.utc) + timedelta(seconds=expiration)
return blob.generate_signed_url(expiration=expiration_time)
return None
def _guess_content_type(self, name):
"""Guess content type based on file extension"""
import mimetypes
content_type, _ = mimetypes.guess_type(name)
return content_type or 'application/octet-stream'
# Convenience function to get a configured Firebase storage instance
def get_firebase_storage():
"""Get a configured Firebase storage instance"""
return FirebaseStorage()
def firebase_upload_path(instance, filename):
"""
Generate upload path for Firebase storage
Format: uploads/{model_name}/{year}/{month}/{day}/{filename}
"""
from datetime import datetime
now = datetime.now()
model_name = instance.__class__.__name__.lower()
return os.path.join(f'storage/{model_name}/{now.year}/{now.month:02d}/{now.day:02d}/{filename}')
# {project_root}/{settings_folder}/settings.py
# use django environ
import environ
env = environ.Env()
environ.Env.read_env('.env')
# update installed apps
INSTALLED_APPS = [
...
'app_storages.apps.AppStoragesConfig',
...
]
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
# Firebase Storage settings
USE_FIREBASE_STORAGE = env.bool('USE_FIREBASE_STORAGE', default=False)
FIREBASE_STORAGE_BUCKET = env.str('FIREBASE_STORAGE_BUCKET', default='')
FIREBASE_PROJECT_ID = env.str('FIREBASE_PROJECT_ID', default='')
FIREBASE_PRIVATE_KEY_ID = env.str('FIREBASE_PRIVATE_KEY_ID', default='')
FIREBASE_PRIVATE_KEY = env.str('FIREBASE_PRIVATE_KEY', default='')
FIREBASE_CLIENT_EMAIL = env.str('FIREBASE_CLIENT_EMAIL', default='')
FIREBASE_CLIENT_ID = env.str('FIREBASE_CLIENT_ID', default='')
if USE_FIREBASE_STORAGE and FIREBASE_STORAGE_BUCKET:
print('using firebase')
# Use Firebase Storage in production
DEFAULT_FILE_STORAGE = 'app_storages.firebase_storage.FirebaseStorage'
STORAGES = {
'default': {
'BACKEND': DEFAULT_FILE_STORAGE
}
}

Comments are disabled for this gist.