A cell phone sitting on top of a table next to a cup of coffee.
Photo by @pawel_czerwinski on Unsplash

Introduction

We can understand Push Notifications as messages sent to one or more applications, frequently displayed — as the name already suggests — as system notifications. They allow users to stay up to date with important information and also draw their attention to certain features.

For the implementation we need a platform that allows sending and receiving messages, also known as a "Messaging" service. Generally we need an implementation on the server side and another on the client side.

Check out a representation of how it works in practice:

Server Side Application Push Notification Provider send User Devices subscribe Client SDK Message Topics send message

In this case we can use:

  1. Cloud Functions or a backend application hosted on a server
  2. Services that provide this functionality, such as Firebase Cloud Messaging
  3. An external provider, such as Novu

What is a Topic?

When it comes to communication between systems, a very common term is message topics, which can be understood as a "channel" through which messages are sent only to that specific place.

Each device or system can subscribe to a topic and thereby receive messages from that channel. For example: one user out of a total of 10 subscribes to a "News" topic in an app — in that case, only that person would receive those messages as notifications.

How Firebase Cloud Messaging Works

As mentioned earlier, we need a service that will be responsible for routing messages to the interested parties — let's consider the Push Notifications scheme.

When using FCM it is necessary to add the Firebase SDK to the project and define the required settings so the app can handle notifications in the foreground or background.

  • The first step to be carried out is initializing the methods for "listening" to events and also generating a token for the application instance.

FCM token storage

This token is used by Firebase when sending messages. The ideal approach is to generate the token when the app initializes and save it in the database, associating that ID with the user — so that the application responsible for sending messages can later retrieve the list of tokens to send to.

Cloud Scheduler

One of the ways to work with sending messages (notifications) to users is through a Cloud Function — which is nothing more than a piece of code that will be executed in response to a specific action. In this case it can be anything, for example:

  • When a user gains a new follower, a notification can be sent to the app;
  • When a product in the shopping cart goes on sale;
  • Send a notification every day;

The possibilities are many. Taking the last case into account, we can "sketch" a simple but effective flow to give us a picture of how this scheme would work:

Cloud Function Firebase Cloud Messaging Runs the function code and send message to FCM Cloud Scheduler Publish a new message every day at 11:00 (UTC) 0 11 * * * deliver Push Notifications topic Triggered when a new message is published

We use something called a Cloud Scheduler to execute a scheduled action. In the example above, every day a new message will be published to a specific topic. Right after that we have our cloud function, which will "listen" for the event — and every time a new message is published to that topic, the code will be executed and will ultimately send a message to users through Firebase Cloud Messaging.

To learn more about the Pub/Sub (Publisher/Subscriber) pattern, check out my other article — Asynchronous programming and Rx-anything

Implementation

For the implementation we will use:

  • Language: Python.
  • Serverless: Firebase (Cloud Messaging and Functions).
  • Flutter (Client Side Application)

The first step is to create the project. For that, Firebase provides a CLI (Command Line Interface) to make the process easier:

  1. Install the CLI according to your operating system — check the official documentation.
  2. Run the command below somewhere to have the initial project files created:
firebase init functions

Given that Python was chosen, the generated structure will be the following:

myproject
+- .firebaserc    # Hidden file that helps you quickly switch between
|                 # projects with `firebase use`
|
+- firebase.json  # Describes properties for your project
|
+- functions/     # Directory containing all your functions code
      |
      +- main.py      # Main source file for your Cloud Functions code
      |
      +- requirements.txt  #  List of the project's modules and packages
      |
      +- venv/ # Directory where your dependencies are installed

Structure taken from the official documentation, check the details here.

Moving on to writing the code that will be responsible for sending messages to Firebase Cloud Messaging, we have the following:

@https_fn.on_request()
def send_message_to_topic(req: https_fn.Request) -> https_fn.Response:
	topic = "app-general-messages"
	androidConfig = messaging.AndroidConfig( ... )

	message = messaging.Message(
			android=androidConfig,
			topic=topic,
		  notification=messaging.Notification(
			title="Notification Title",
			body="Notification Body"
		),
		data={"type": "notification"}
	)

	try:
		messaging.send(message)
		return https_fn.Response("Message to FCM was sent with success!")

    except Exception as e:
			return https_fn.Response("Message was not sent!", status=500)

We can notice the use of the @https_fn.on_request() notation — this is one of the possible "triggers" provided by Firebase for invoking a method. In this case it will be triggered via an HTTP call to the function's address (which will be generated after deployment).

As mentioned earlier, there are countless possible "triggers" for executing the function. A very useful example would be for implementing a Scheduler:

@pubsub_fn.on_message_published(topic="pubsub-topic")
def on_message_published(event: pubsub_fn.CloudEvent[pubsub_fn.MessagePublishedData]) -> None:
  return ...

That is, whenever a new message is published to the pubsub-topic topic, this method would be invoked. Cloud providers generally offer the ability to configure this scheduling and send a message to a chosen topic.

After finishing the code implementations, we can deploy using the Firebase CLI:

firebase deploy --only functions

The command above will perform the deployment and make the cloud function available directly in the account previously defined at the start of the process.

With the server side complete, we can move on to implementing the SDK on the client side. We'll use a Flutter app as an example — we'll also need to add FCM as a dependency to the project. We can have a service that will handle the push notification processes:

abstract class NotificationService {
	static final _messagingService = FirebaseMessaging.instance;

	static Future<void> setUpNotifications() async {
		await _requestNotificationPermission();
		await _getToken();

		await _messagingService.setForegroundNotificationPresentationOptions(
			alert: true,
			badge: true,
			sound: true,
		);

		await _localNotification.initialize(
			NotificationConstants.settings,
			onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
		);

		await _subscribeToGeneralTopics();
		_setupMessageListeners();
	}

	static Future<void> _subscribeToGeneralTopics() async {
		await FirebaseMessaging.instance.subscribeToTopic(
			'general-messages',
		);
	}

	static void _setupMessageListeners() {
		FirebaseMessaging.onMessage.listen((message) {
		  _handleForegroundMessage(message);
		});

		FirebaseMessaging.onBackgroundMessage(_handleBackgroundMessage);
	}
}

It is important here to subscribe the current device (or session) to the topic defined earlier. When a new message becomes available, Firebase will forward it to all devices subscribed to that topic.

After defining the necessary methods for initializing the instance and registering the token, the methods declared in _setupMessageListeners will be invoked and will display the received message as a system notification.

If you want to understand further details, I suggest checking the official documentation — there are good and comprehensive examples there.

Some implementation details were omitted to keep the reading flow dynamic and to the point. I hope this article has contributed a little more to your technical knowledge and inspired you to explore the serverless journey a bit further — there is still a lot to discover. Happy coding!! :)