Build a serverless PUSH and SMS Notification Service on AWS

Lorenzo Panzeri
9 min readMay 13, 2023

--

I’m trying to achieve more with my side projects working by iterations: better done than perfect. This is the first iteration of my new serverless microservice that can take care of user notifications. For now it manages sending real time PUSH and SMS, I think it’s enough to cover common needs so I’m shipping it.

In the future I want to add more features like:

  • Control Dashboard
  • Email management
  • Scheduled notifications
  • Usage Plans with Api Keys

As usual you will find this project on my GitHub

Sending Pushes to mobile applications it’s a very common business requirement for every project. As I’m trying to walk the path of Software Architecture, let’s stop and think about a simple and clear solution.

If a customer puts Push notification as a requirement in a new application there are a lot of chances that this requirement will start to appear in every following project. Maybe there will also the possibility that you get to update older applications to add this feature.

As Architects we cannot permit that every Application Team chooses its own way to manage Push notification and/or SMS: this will lead to draining development time in every team to reinvent the wheel … and every wheel will have a different shape 😀 .

The goal is one common implementation that serves this feature easily to each client application that needs it. Here my project gets its purpose.

To design this application we have to figure out the mandatory features to implement:

  1. Identify the customer’s device where to send
  2. Actually send the notification
  3. Providing a list of notifications for each customer
  4. Setting the notification as read by the customer

These are only the basic functionalities but to get things done it’s a good starting point.

The dream team is AWS SAM, Api Gateway, Lambda, DynamoDB and SNS

PairLambda: Identify the customer’s device where to send

PairLambda:
Type: AWS::Serverless::Function
Properties:
Handler: it.loooop.Pair::handleRequest
CodeUri: .
Environment:
Variables:
DYNAMODB_DEVICES_TABLE: !Ref DevicesTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DevicesTable
Events:
ApiEvents:
Type: Api
Properties:
Path: /pair
Method: POST
RestApiId: !Ref NotificationServiceAPIGateway

To send a notification to a device we need a token. This token can be provided by Google Cloud Messaging for Android devices (and also for iOS) and by Apple Push Notification Service (APNS). AWS SNS manages both for us so we only need to persist the device token (and the Customer identification data) and pass it to SNS when needed.

For persistence we will use DynamoDB, creating the Devices table:

#Table with customer devices and Firebase tokens
DevicesTable:
Type: AWS::Serverless::SimpleTable
Properties:
TableName: Devices
PrimaryKey:
Name: deviceId
Type: String

The request for this Pair API is:

{
"identificationId":"lorenzo@test.it",
"appId":"APP1",
"type":"ANDROID",
"token":"48h1FCM-Code0IMUtT:Customer-Device-Fcm-Token-iFgqs5_gZcnQX3gGTTzcxL2_iZ0BU7aZk4tAGwnopaCbFsmu-a8rh4lXBtopZHQ4fQrFbLCKjofurehEqm5_VlW1Stxaag_FvWZikdjCMc_4N",
"deviceInfo":"SAMSUNG:S10"
}

IdentificationId has to be an unique identification key for your customer, can be an email, a contract number: ideally is your business key to identify the unique user.

AppId is the name (or code) of the client application and has to be chosen with the Notification Service Team because there is some configuration behind it (see later configuration of Firebase and SNS)

Token is in this case the Firebase Token (GCM, Google Cloud Messaging)

Pay attention to the persistence layer: we will use a “trickto manage data inside the DynamoDb Table:

You see that I put more than an info inside the deviceId field, with # used as a separator? It’s a common way to handle the partition key in DynamoDb. In my previous article there is the link to an Alex DeBrie interesting talk where he explains this approach.

SendLambda: Actually send the notification

SendLambda:
Type: AWS::Serverless::Function
Properties:
Handler: it.loooop.Send::handleRequest
CodeUri: .
Environment:
Variables:
DYNAMODB_DEVICES_TABLE: !Ref DevicesTable
DYNAMODB_NOTIFICATIONS_TABLE: !Ref NotificationsTable
DYNAMODB_APPS_TABLE: !Ref AppsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DevicesTable
- DynamoDBCrudPolicy:
TableName: !Ref NotificationsTable
- DynamoDBCrudPolicy:
TableName: !Ref AppsTable
- Statement:
- Effect: Allow
Action:
- 'sns:*'
Resource:
- '*'
Events:
ApiEvents:
Type: Api
Properties:
Path: /send
Method: POST
RestApiId: !Ref NotificationServiceAPIGateway

This is the more complex part of the project. We need to interact with all the DynamoDb tables in the project and with SNS.

As you may know AWS SAM has a List of pre-built policies that are easy to apply but if needed, you can see in this example how to apply a custom Policy to access a resource.

The request for this API can be configured in two ways:

SMS

{
"identificationId": "lorenzo@test.it",
"appId": "APP1",
"type": "SMS",
"mobileNumber": "+393381234567",
"message": "Test sms message"
}

PUSH

{
"identificationId":"lorenzo@test.it",
"appId":"APP1",
"type":"PUSH",
"title":"Push title test",
"message":"Push text message test"
}

As you can see, we provide identificationId, appId and type that are what we need to create the Partition key.

ListLambda: Providing a list of notifications for each customer

 ListLambda:
Type: AWS::Serverless::Function
Properties:
Handler: it.loooop.List::handleRequest
CodeUri: .
Environment:
Variables:
DYNAMODB_NOTIFICATIONS_TABLE: !Ref NotificationsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref NotificationsTable
Events:
ApiEvents:
Type: Api
Properties:
Path: /list
Method: POST
RestApiId: !Ref NotificationServiceAPIGateway

Before going on with this Lambda we need to stop and think about our data access pattern: we need to search for notifications of a particular user in a certain time range. Translating this requirement to DynamoDb means that we need a Partition Key AND a Sort Key.

This digression will suits me also for showing you the last two DynamoDB tables that compose the persistence of this project.

Let me explain better:

This is the Apps table. It stores all the Sender Application that our Notification microservice can serve. It’s clear that we only need a Partition Key for this access pattern, only the “appId” identificator, so we can user the SAM’s SimpleTable definition:

#Table with App information and configuration for SNS
AppsTable:
Type: AWS::Serverless::SimpleTable
Properties:
TableName: Apps
PrimaryKey:
Name: appId
Type: String

Now we need to define out Notifications table that will be the “main” table of the project.

If you need also a Sort key you have to rely on the “real” DynamoDB::Table like here for the Notifications table:

#Standard CloudFormation DynamoDb table definition because SimpleTable doesn't support orderKey
#Table with the notifications sent to customers
NotificationsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Notifications
AttributeDefinitions:
#CompositeKey
- AttributeName: notificationId
AttributeType: S
- AttributeName: insertTimestamp
AttributeType: S
#- AttributeName: readTimestamp
# AttributeType: S
#- AttributeName: message
# AttributeType: S
#- AttributeName: device
# AttributeType: S


KeySchema:
- AttributeName: notificationId
KeyType: HASH
- AttributeName: insertTimestamp
KeyType: RANGE


BillingMode: PAY_PER_REQUEST

Also there we will use a “compositePartition key notificationId that is built like this:

<user_identification_id>#<client_application_code>#<sms_or_push>

But as I told you before, for the List functionality we also need a Sort key to filter and order the results and here you can find the “insertTimestamp” for this purpose.

The Primary key of a DynamoDB table with Partition key and Sort key is a combination of these two both:

<primary_key> = <partition_key> + <sort_key>

I hope that is clear now 🙂

The request for this API is:

{
"identificationId":"lorenzo@test.it",
"appId":"APP1",
"type":"PUSH",
"from":"2023-04-22 17:10:00.000",
"to":"2023-04-27 17:10:10.000"
}

You can see that we provide all what is needed for the Partition Key and a range of datetime for the Sort key condition.

ReadLambda: Setting the notification as read by the customer

ReadLambda:
Type: AWS::Serverless::Function
Properties:
Handler: it.loooop.Read::handleRequest
CodeUri: .
Environment:
Variables:
DYNAMODB_NOTIFICATIONS_TABLE: !Ref NotificationsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref NotificationsTable
Events:
ApiEvents:
Type: Api
Properties:
Path: /read
Method: POST
RestApiId: !Ref NotificationServiceAPIGateway

This API will be called by the mobile app to set a notification as read. Needs only the Notifications table as a resource.

The request is:

{
"notificationId":"lorenzo@test.it#APP1#PUSH",
"insertTimestamp":"2023-04-23 09:03:47.015"
}

As you can see, notificationId is the “compositePartition key and insertTimestamp (that is provided in the ListLambda response, we will see this better later) as Sort key. Partition key and Sort key combo has to be unique in the table.

Now let’s see this Microservice in action

Deploy it to AWS using SAM commands:

sam build
sam deploy –guided

Or (if you already have the samconfig.toml file)

sam deploy

samconfig.toml for reference:

version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "micro-notification-service-2"
s3_prefix = "micro-notification-service-2"
region = "eu-west-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
profile = "your-aws-profile"
disable_rollback = true
image_repositories = []
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-abc12345defg"

I have also messed around with Flutter last year to build a mobile app maybe in the future I will speak about it but at the moment the backend of this app is homeless because Heroku free plan war discontinued last year 🙁.

This messing around helped me to build a little Flutter app to test the Push notifications:

Wait, I forgot about SNS and Firebase configuration

Firebase

You only need to follow the guided procedure to start a new Flutter project. You will have to install the Firebase CLI that will also set up for you your Flutter environment and it will also take care to put the correct secrets in the right place.

You will only have to activate the “API Cloud Messaging (legacy)” token:

This token will be used to create the “Platform Endpoint” in SNS

SNS

Go to SNS and select “Push notifications” on the left sidebar, then click the “Create platform application” button:

The correct way here is to create a Platform application for each of the Sender Application your microservice will serve. My FLutter application is called “notification-gateway-2” as this very same project.

Put the name to your Sender Application and put the Firebase Token in the API Key box. Complete the creation by clicking the “Create platform application” button.

Now click on the just created Platform endpoint and copy the ARN.

Let’s go to DynamoDB and create an App configuration for this endpoint:

Create a new Item and in this item add 2 more fields called snsArnAndroid and snsArnIos.

In the first put the ARN of the Platform Endpoint and Create item

Ok all configurations are done!

Let’s try it out

First we need the Mobile App Device token: I implemented the REST call on the startup of the Flutter app that calls the Pair REST API and register the device but for this test let’s proceed manually by copying it from the Console:

Put this token in the Postman request for the Pair API:

Ok now we have registered the device and we can check it also on the Devices table:

Let’s send a PUSH to it:

It works! 🙂

The notification is also persisted on the Notifications table:

And SNS has create a Device endpoint:

Now lets retrieve the Notification list for the same customer:

Here it is our notification that you can say it’s still unread. Let’s mark it as read:

If we call another time the List API we will see that our notification is marked as read:

All these basic features are working! 😀 We have finally built our Serverless Notification Microservice!

That’s all Folks! Remember to cleanup your AWS account:

sam delete --stack-name micro-notification-service-2

I will not show you the SMS sending because it is out of the Free Tier but it works. If you only need a basic SMS feature please see this article of mine.

Thank you for reading, see you next project!

ps: this article took me a lot of time and I struggled to finish it so, if maybe some details are missing, please feel free to write to me.

Lorenzo 🙂

--

--

Lorenzo Panzeri

Passionate Developer - Compulsive learner - Messy maker