Skip to content

Using Firebase to send real-time notifications in Django apps

Updated: at 07:58 PM

How to add real-time notifications in Django applications | abdadeel

Table of contents

Open Table of contents

Introduction

Recently, I have been exploring quite a few ways to add real-time functionality in Django applications. During my hunt, I came across many third-party services like PubNub, Pusher, and now Firebase which I think is the most exciting of all.

Following are the rubrics that I standardized during my decision and comparison to even custom solutions like django channels. Let’s see how firebase fits these points.

🚀 Let’s Build A Demo Django App

In this article, I will guide you through all the steps you need to follow to successfully leverage firebase firestore to securely send real-time notifications to users in your django applications. During my testing, I have built a demo application that you can refer to any time you find yourself lost. Here is the Github 😸 link 👇

https://github.com/mabdullahadeel/django-firebase-notifications

Here is the live app 👇

https://django-firebase.vercel.app

Server Logic

The first problem that you might face when trying to implement firebase with your django application is authentication. Your users are currently being authenticated by your django application, how you would pass that auth state to firebase so it knows about the user? To solve this, the solution is custom tokens. When the client successfully login with you django app, firebase admin SDK can be used to create a token for the client. The client will use this token to connect to firebase. Here is how the flow goes.

Flow chart for auth

See the full diagram here

Let’s start with the user login. They put in their credentials to log into the frontend application. The client sends the credentials i-e username/email and password to the backend to exchange for a token(JWT or simple token). The actual auth implementation might vary from application to application but in my demo, I am using DRF token base authentication. On successful login, the server generates two tokens as seen in the flow chart above.

Sending the Notification

Let’s go through the setup.

Here I assume that you have a firebase account which you do if you have a google account. To send notifications from django application, we are going to use firestore. If you have not already set up a firebase project or firestore, I would recommend going throughthis quickstart guide.

To communicate with firebase through admin SDK, you need service account credentials. To generate service account creds, go to the Service account in your firebase project console. Make sure you have selected the right project. Follow the following 👇

Image description

On success, you will have a .json file downloaded. Let’s rename that file to credentials.json . Make sure to keep this file safe since the credentials in this file allow access to all firebase resources.

Set up admin SDK instance

Run the following pip command to install firebase SDK in your django project.

pip install firebase-admin

Then initialize the admin app instance providing the path to the credential.json file. If this file is in your project directory. Make sure to add credentials.json to your .gitignore file. In my case, I created a helper class FirebaseService in core/firebase/firebase_service.py to handle all of the firebase-related logic.

import logging
from typing import Any, Dict
from uuid import uuid4
from django.conf import settings
from django.core.cache import cache

import firebase_admin
from firebase_admin import credentials, auth, firestore

from users.models import User

cred = credentials.Certificate(settings.GOOGLE_APPLICATION_CREDENTIALS) # path to credentials.json file

firebase_app = firebase_admin.initialize_app(cred)
auth_client = auth.Client(app=firebase_app)
firestore_client = firestore.client(app=firebase_app)

logger = logging.getLogger(__name__)


def cached(func):
    def wrapper(*args, **kwargs):
        user = kwargs.get('user')
        key = 'token_' + str(user.id)
        token = cache.get(key)
        if token is None:
            token = func(*args, **kwargs)
            cache.set(key, token, timeout=60 * 60) # 1 hour
        return token

    return wrapper

class FirebaseService:
  @staticmethod
  @cached
  def get_custom_token_for_user(user: User):
    auth_claims = {
      'uid': user.id,
    }
    return auth_client.create_custom_token(uid=user.id, developer_claims=auth_claims)

  @staticmethod
  def send_notification_to_user(user: User, message: Dict[str, Any]):
    msg_id = str(uuid4())
    notification_ref = firestore_client.collection(u'app-notifications') \
      .document(u'{}'.format(user.id)).collection("user-notifications").document(u'{}'.format(msg_id))

    notification_ref.set({
      u'message': message,
      'id': msg_id
    })
    logger.info(u'Notification sent to user {}'.format(user.id))

Here I am loading the GOOGLE_APPLICATION_CRENDITALS which is the path to the credentials.json file from the django settings. And in my settings.py file, I am loading the same variable from the environment using django-environ. You can have a look at that here.

Image description

Here is the firestore rule definition if you’re interested.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match/app-notifications/{user_id}/{document=**} {
      allow delete, read: if
          request.auth.uid == user_id
    }
  }
}

Client Side Logic

On the client, install firebase.

yarn add firebase
# or
npm install firebase
# or
pnpm add firebase

Then in ./firebase/index.ts , I am using the credentials to initialize the firebase app and use it across the application.

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FB_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FB_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FB_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FB_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FB_MSG_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FB_APP_ID,
};

export const app = initializeApp(firebaseConfig);
export const firestore = getFirestore(app);
export const auth = getAuth(app);

Then in my authentication logic, on successful login/signup I am automatically signing the user in with firebase using the token received from the django server. See here for details

import { auth as firebaseAuth } from "./firebase";

const initializeFirebaseAuth = async (user: MeResponse) => {
  return signInWithCustomToken(firebaseAuth, user.fb_token);
};

To display notifications on the UI, I create a custom hook that subscribes to the appropriate channel to receive notifications for the currently logged-in user.

import React, { createContext, useEffect, useState, useContext } from "react";
import { doc, onSnapshot, deleteDoc, collection } from "firebase/firestore";
import { firestore } from "../firebase";
import { useAuth } from "../hooks/useAuth";

interface NotificationPayload {
  message: string;
  id: string;
}

interface NotificationState {
  messages: NotificationPayload[];
  unreadCount: number;
  resetUnreadCount: () => void;
  markAllAsRead: () => void;
  markOneMessageAsRead: (id: string) => void;
}

export const NotificationsContext = createContext<NotificationState>({
  messages: [],
  unreadCount: 0,
  resetUnreadCount: () => {},
  markAllAsRead: () => {},
  markOneMessageAsRead: () => {},
});

export const NotificationProvider: React.FC<React.PropsWithChildren> = ({
  children,
}) => {
  const [messages, setMessages] = useState<NotificationPayload[]>([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const { isAuthenticated, user } = useAuth();

  useEffect(() => {
    let unsubscribe: () => void;
    if (isAuthenticated && user?.user) {
      unsubscribe = onSnapshot(
        collection(
          firestore,
          "app-notifications",
          user.user.id,
          "user-notifications"
        ),
        snapshot => {
          const messages = snapshot.docs.map(doc => ({
            message: doc.data().message,
            id: doc.id,
          }));
          setMessages(messages);
          setUnreadCount(prev => (messages.length ? prev + 1 : 0));
        }
      );
    }
    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, [isAuthenticated, user]);

  const resetUnreadCount = () => setUnreadCount(0);

  const markAllAsRead = async () => {
    if (isAuthenticated && user?.user) {
      await Promise.all(
        messages.map(async msg => {
          await deleteDoc(
            doc(
              firestore,
              "app-notifications",
              user.user.id,
              "user-notifications",
              msg.id
            )
          );
        })
      );
      setMessages([]);
    }
  };

  const markOneMessageAsRead = async (id: string) => {
    if (isAuthenticated && user?.user) {
      await deleteDoc(
        doc(
          firestore,
          "app-notifications",
          user.user.id,
          "user-notifications",
          id
        )
      );
    }
    setMessages(messages.filter(msg => msg.id !== id));
  };

  return (
    <NotificationsContext.Provider
      value={{
        messages,
        unreadCount,
        resetUnreadCount,
        markAllAsRead,
        markOneMessageAsRead,
      }}
    >
      {children}
    </NotificationsContext.Provider>
  );
};

export const useFirebaseNotifiactions = useContext(NotificationsContext);

Conclusion

Exploring this new way of implementing real-time features was exciting and its no doubt one of the best options when it comes to critical factors like scalability, maintainability, and time to implementation. If you want to test it, here is the live demo👇

https://django-firebase.vercel.app

If you like what you just read, why not throw a follow on Twitter abdadeel_

Thanks 👋