In this article, you’ll learn how to build a GitHub tracker that notifies users when there’s a new Issue/PR on a tracked repository by sending push notifications.
GitHub already sends notifications through emails if you’ve opted in, but many studies have shown that push notifications reach users better than emails do. After you’ve built the GitHub tracker by following this tutorial, you’ll have learned how to:
There are a few skills and services you’ll need to follow this article:
Let’s take a look at what these so called “push notifications” are.
You must be familiar with regular notifications. These are little bubbles of text that appear on your screen to notify you of something. Push notifications are similar, except that they aren’t generated on-demand, but they are generated upon receiving push events. Push notifications work when an app is closed, while regular notifications require you to have the app open.
Push notifications are used in chat applications to notify users when they have unread messages, in games, to notify users of game events, in news sites, to notify users of breaking articles, and for many other purposes.
There are four steps to show push notifications in your app:
Let’s use Svelte with Vite.js instead of Rollup in this article. Vite is, as its name suggests, faster than Rollup, and also provides built-in support for environment variables. To create a new project with Svelte and Vite, run this command:
Select the framework to be
cd into the project folder and you can add TailwindCSS to your application and install all dependencies with these commands:
Finally, open the project in your favorite code editor and run
npm run dev or
yarn dev to start the application on http://localhost:3000.
We’ll use the GitHub API to get a list of issues and pull requests for a repository that the user has tracked. A user’s tracked repositories and their username will be stored in the MongoDB database.
The first step would be to prompt the user for their username. Create
src/lib/UsernamePrompt.svelte, which will be the component that will do so. Here’s my UI for the form, but you can design it however you want:
Add this component in
App.svelte like so:
Next, let’s add the main tracker UI. Create file
src/lib/Tracker.svelte and add the below code in it:
To test out your component, temporarily swap out the
UsernamePrompt component for the new
Tracker component in
Your screen should now look like this:
Note: remember to restore
App.svelte to its previous code!
We need to have a back-end server to send push events to our application. This means that you need to create a new (maybe) ExpressJS project, and then deploy that separately. This will all be a headache for someone just experimenting with push notifications.
Vercel Cloud Functions to the rescue! Cloud functions are like Express routes. They can run code and give you a response when you fetch its URL. Vercel has support for cloud functions; you just have to create files in the
api folder. You’ll be using cloud functions to interact with MongoDB, since exposing secrets client-side is never a good thing.
First, make sure you have a Cluster in MongoDB Atlas. MongoDB has a free plan (M0), so be sure to create one if you haven’t already. Now, go to the Database Access tab in the sidebar of your Atlas dashboard. Add a new Database User by clicking the green button on the right side. Enter in the user’s details (don’t forget the password), and create the user.
To connect to the database, you’ll need the connection string. Save the new user and password somewhere and head to your Cluster’s Overview. Click the Connect button on the right side and select Connect your Application as the method of connection. You should see a connection string similar to the one below.
Now that you have the connection string, you can connect to your database, but first, you need to deploy the current application to Vercel. The easiest way to do this is using GitHub.
Create a new GitHub repository and push your code to it. Next, head to your Vercel Dashboard and click the New Project button. Import your GitHub Repository, make sure the framework is Vite, and add an environment variable called
MONGODB_URL. Set its value to the connection string of the MongoDB database.
Once your website has been deployed, you need to change your local development command from
yarn dev to
vercel dev. Upon running the command, if you’re asked to link to an existing project, click yes.
Note: make sure to install the Vercel CLI with
npm i -g vercel if you haven’t already.
Like me, if you run into a problem with using
vercel dev, be sure to change the Development Command of your project to
vite --port $PORT from
vite in the Vercel Dashboard.
This will allow us to use cloud functions with the correct environment variables locally.
Let’s add a helper file that will allow us to access MongoDB without opening up too many connections. Create file
api/_mongo.js and put the following code in it. A file in the
api directory that is prefixed with a
_ will not be treated as a cloud function. This allows us to add helpers and other logic in separate files:
Exporting the connection promise instead of the main client itself will prevent us from having redundant connections, since we’re working in a serverless platform.
Notice how I am using
require instead of
import? This is because, as of the time of writing, Vercel Cloud Functions doesn’t support ESModule
There’s one problem here. If you see the
package.json of our app, you’ll notice that it has a line
api directory as CommonJS files, so we can use the
require statement, create
api/package.json and add this line in it:
This will now allow us to use
require statements in the
api directory. Install the MongoDB connection driver with this command:
The tracker, as of now, doesn’t really work, so let’s fix that.
For authentication, we need to store the username that the user has input in the MongoDB database.
Create a file
/api/storeusername.js. This will be a cloud function and will be mapped to
http://localhost:3000/api/storeusername. Put the below code in it:
Next, get the MongoDB client like so:
username from the request’s body:
Next, you need to store this username in the database:
Finally, this is how the
api/storeusername.js file should look:
Deploy your application to Vercel with
vercel ., or by pushing to GitHub, and your serverless function should be live! You can test it using cURL with this command:
This should create a new document in the
users collection with the
_id field being the username we just gave.
Now all that’s left is to fetch this function on the front end. In
src/lib/UsernamePrompt.svelte, in the
submit function, first you need to send a request to the cloud function, and then put the username in
localStorage, so we know that the user is authenticated. You can send requests with the
We’re reloading the page, because in
App.svelte, when the page is loaded, we need to check if there is a username in
localStorage. If there is, we can skip the
UsernamePrompt screen. To do so, add this code in the
script tag of
The above code will check the
localStorage for a username and set
true if it exists. Next, all we have to do is update the DOM. Right under the
script tag of
App.svelte, add this:
Now let’s add the functionality for the actual tracking features of the tracker. If you open
Tracker.svelte, you’ll notice there are two functions —
untrack(). These functions should track and untrack repositories respectively, by adding them to the database.
But before that, you need to add a few more cloud functions. One to track a repository, another to untrack, and one last to get a user’s tracked repositories.
Let’s work on them one by one.
api/trackrepo.js. This will be mapped to
When a user wants to track a repository, they’ll send a
POST request to this function with the name of the repository and their username in the body. The function will add the name of the repository in the
trackedRepos field of the
users collection. Add some code to get these fields from the body:
And finally, add the code to track the repository by adding it to the database:
And this is how
api/trackrepo.js should look:
Now it’s time to use this function in the tracker. Open
src/lib/Tracker.svelte and change the
track() function to this:
Now, when you enter a repository in the input and click Track, it should get saved in the database.
Let’s add a cloud function to untrack a repository. Create file
api/untrackrepo.js. This will be mapped to
The request body of this cloud function will be the same as that of the
trackrepo function — the user’s
username and the
Next, here comes the code to delete the repository from the user’s
And this is how
api/untrackrepo.js should look:
It’s now time to utilize this cloud function on the front end. In the
untrack() function of
src/lib/Tracker.svelte, add this code:
You’ll notice that it’s very similar to the
track() function, because it’s literally the same; just the URL has been updated. You can’t really test this out just yet, because we aren’t displaying a list of the tracked repositories, so let’s fix that.
This part is pretty simple. You just need to fetch the user’s tracked repositories from the database and display it on the front end. Create a cloud function
api/listrepos.js and add the following code to it:
Since the cloud function will be called using an HTTP
GET request, you can’t put a body in it, so we’re using the query string to pass the username; and since
user.trackedRepos can be
null, we’re making sure to return an array. Next, it’s time to use this cloud function on the front end! Create an
async function called
fetchRepos in the
src/lib/Tracker.svelte file. This function will be responsible for fetching the user’s tracked repositories from the database using the cloud function we just created:
We need to fetch this function when the component is mounted. This can be done using the
onMount hook in Svelte. When the component is mounted, I want to set the returned value of the above function to a variable called
trackedRepos, so we can use it in the DOM:
Now that we have access to the user’s tracked repositories, let’s update the HTML template in
Tracker.svelte to show an accurate list of tracked repositories:
We still have to reload the page to see any changes. Let’s fix that by updating the DOM every time the
untrack buttons are clicked:
And here’s how
Tracker.svelte should look:
And here’s a screenshot of how the app should now appear.
Push notifications are only supported on installed apps. Yes, you can install web applications as regular applications using supported browsers — namely, Chrome and other Chromium-based browsers.
To make an app installable, you need to convert it into a progressive web app. This is a three-step process:
If all three steps are completed, an install button will appear on the address bar when you visit the application.
sw.js. This file should be publicly served like your CSS, so put it in the
Service workers work by listening to events. For caching files, so your app works offline, you’ll listen to the
fetch events. The
install event gets called when the service worker gets installed. The
activate event gets called when the service worker is running, and the
fetch event gets called whenever a network request is made. Event listeners can be added using
self.addEventListener(). Let’s create a
public/service-worker.js file and add the following code to it:
All that’s left is to register this service worker. We’ll do that in the
onMount function of
App.svelte. Add this code at the end of the callback inside
The above code first checks for service worker support in the browser, and then registers our service worker. It has to be noted that the path in the
register() function is the path relative to your domain, not to the project folder — meaning that the service worker should be accessible at
http://localhost:3000/service-worker.js, which it is, since it’s in the
Now if you reload the page and open the console, you should see the above messages.
To make an app work offline, you need to cache its contents using a service worker. Since our app makes requests to cloud functions, it can’t really do much when there’s no network. So instead of displaying a cached, functionless version of the app, let’s display a page that indicates that we’re offline. Create a
public/offline.html file and put the following code in it:
Feel free to customize this page however you want. You now need to cache this page. Caching is also a three-step process that uses the three above service worker events that we listened to. Here’s how it works:
The cache is opened and desired routes are added to the cache using
cache.add. This happens during
The older cache is deleted, so only the latest is saved to the user’s computer. This utilizes less storage. This happens during
We intercept any network requests and check if those requests are page navigations — that is, changing routes. If the request succeeds, it’s all well and good, but if the request fails, we deliver the
offline.html page to be displayed to the user. This happens during
Let’s implement the first step. Open the service worker file and change the
install event’s handler like so:
event.waitUntil() is a function that’s similar to the
await keyword. Callbacks of
addEventListener can’t be asynchronous, so to implement that functionality, we should use
event.waitUntil() and pass it a promise so that the promise will get awaited.
self.skipWaiting() tells the browser that we’re done with the
install process, so activate the service worker. Speaking of
activate, let’s now add the code to delete any old caches:
And with that, the
offline.html page should be cached. To double-check, open the developer tools by pressing F12 and select the Application tab. On the sidebar, there should be a Cache Storage tab. Click on it and you should notice
Now all that’s left to do is to serve this file when there’s no network:
event.respondWith() function will respond to the network fetch request with whatever
Response object is passed to it. In this case, we’re fetching the request first, and if the request fails, which will most likely be because of an internet problem, we’re sending the
offline.html page, which was cached by the service worker.
Now refresh the page and turn off your Wi-Fi or Ethernet. You should now see our offline page instead of the default chrome “No network” page when you refresh. This offline page unfortunately doesn’t have the dinosaur game, but it does enable us to install the application as a PWA.
Here’s how the service worker should look:
manifest.json, or web manifest, contains some useful information about your application — things like the app’s name, its theme color, a description, its icons and much more. This file is usually called
manifest.json and must be linked to your website using the
<link> tag in the HTML, like how you link CSS files. Let’s add a manifest for our application. Feel free to use a generator for this one:
You need to download a bunch of icons for the application. These icons are of different sizes and are used by different operating systems. You can download them from the source code repository or by using this link. Be sure to extract the ZIP file to
Next, you need to add the manifest and the icons to the
index.html file. You can do so by putting the following code in it:
Open Chrome’d developer tools by pressing F12 and head to the Lighthouse tab and create a new audit. You should now get an “Installable” score on the PWA section. This means that you have successfully converted your website to a webapp, and you can now install it by clicking the button on the address bar.
Before we can send push notifications, we need to get permission from the user. You can use the
Notification.requestPermission() method to do so. This method is asynchronous and returns a string that can be equal to
granted. These are returned when the user either presses the X, presses Deny or presses Allow on the notification prompt, respectively. We’ll use the
onMount hook in
App.svelte to call this function:
You should now get a popup asking you to allow notifications in the app. Now that we have permission to send notifications, let’s use the service worker to subscribe to push events. This can be done using the
pushManager.subscribe() function of the service worker. You can either do this in the service worker itself, or after registering the service worker in
App.svelte. I’ll go with the latter, so if you want to do the same, just replace the
navigator.serviceWorker.register function in
onMount with the code below:
If you open the console, you’ll notice an error saying that the
applicationServerKey is missing. Push notifications need servers to send them push messages, and these servers are authenticated with VAPID Keys. These keys identify the server and let the browser know that the push message is valid. We’ll use Vercel Cloud Functions to send push messages, so we need to set it up.
We’ll use the web-push npm package to help us generate keys and send push events. To install it,
cd to the
api folder and run the following:
cd to the
api folder, as otherwise the
web-push package will be installed in the Svelte app.
To send push notifications, you’ll need to generate a public and private VAPID key pair. To do so, open the Node REPL using the
node command and run the following commands:
Copy these two keys and store them as environment variables on Vercel. Be sure to call them something memorable like
Now, we can start work on the cloud function. Create file
api/vapidkeys.js. This file will be responsible for sending the public VAPID key to the client. You should never share the private VAPID key. In
api/vapidkeys.js, first we need to initialize
Be sure to replace
YOUR_VERCEL_DOMAIN with your Vercel app’s domain. Next, let’s export a function to just return the public VAPID key to the requester:
With that done, you can now update the
onMount function in
App.svelte to first fetch the cloud function to get the public key, and then use the public key in the
Notice how we’re only fetching the VAPID keys if we haven’t subscribed to push notifications. If you open the console, you should see the subscription logged to the console.
The endpoint that is provided is very important to us. This endpoint will allow us to notify this user using
web-push. Let’s create a cloud function to store this endpoint in the database. Create file
Let’s grab the
subscription and the username from the body:
And let’s add it to the database:
And here’s how the final cloud function should look:
This function should be called every time we subscribe to push notifications. Let’s use a Svelte reactive block to call this cloud function every time the
sub variable has a value and the
isLoggedIn variable is true. Add this code just before the ending of the
<script> tag in
Refresh the page, and you should see that the current browser’s push endpoint and keys are stored in the MongoDB database in the
All you have to do is handle the
push event in the service worker and create a cloud function to check GitHub for new issues and PRs.
Let’s do the latter first. Create a new cloud function
api/fetchgh.js. This function will be responsible for checking GitHub and sending push notifications:
Let’s get all the users from the database, so we know what repos to fetch:
Next, create two variables to store the currently fetched repositories, and the repositories with any new issues or PRs:
For each user, let’s check their tracked repositories for any new issues. To make sure that one repository is checked only once, we’ll add the repository to
alreadyFetchedRepos, and we’ll add any repositories that have new issues to
reposWithIssues. To do so, we need to loop over every user in the
users array and get a list of repositories to fetch. This will be done by checking their
trackedRepos for any duplicates. Once that’s done, we’ll call the
fetchRepo function for every repository.
fetchRepo will return a Boolean —
true if there are new issues,
fetchRepo will be asynchronous, I’ve used
map to return promises every time and awaited them all using
Promise.all. This works because the
for loop is asynchronous. If promises aren’t awaited, variables can be
undefined, so be sure to await promises!
Now for the
fetchRepo function. This function will get the last time we’ve checked the GitHub API from the database. This is to only get the latest issues from GitHub. It then fetches the GitHub API for any new issues, and returns a Boolean value if there are any:
Once that’s done, we need to send push notifications to any user that has tracked a repository that has any new issues. This can be done using
web-push. Add these lines of code to the end of the exported function:
First, we need to check if any of the user’s tracked repos have new issues. This can be done with the
Array.some() determines whether the specified callback function returns
true for any element of an array, so we can easily use this to check:
And finally, we send the notification:
And here’s how the cloud function should look:
All that’s left to do is to listen to
push events in the service worker. Open the service worker and add the code below:
When you call the cloud function, maybe using cURL, you should see
new-issue logged in the browser console. That isn’t really very helpful, so let’s make it send a notification:
fetched collection from MongoDB and call the cloud function again. You should now receive a notification from the web browser.
Deploy the application using
vercel . or by pushing to GitHub, install the app as a PWA, and run the cloud function by going to
https://YOUR_VERCEL_APP/api/fetchgh and you should receive a notification, even if you haven’t opened the application!
If you don’t receive the notification, or you get a
410 error from web push, be sure to allow the notifications
forever in the prompt when you get asked.
The tracker isn’t really a tracker if we have to manually call the cloud function, right? Let’s use EasyCron to call the cloud function automatically every hour.
Head to your EasyCron dashboard and create a new CRON job. For the URL, enter
https://YOUR_VERCEL_DOMAIN/api/fetchgh, and choose an interval. I’ll go with every hour, but feel free to customize it however you like.
And with that, you should be getting notifications every time there’s a new issue/PR in any of your tracked repositories. Feel free to check out the source code or the live version if you’ve gotten stuck anywhere.
I am a sixteen-year old self-taught web developer from India.
A practical guide to leading radical innovation and growth.
© 2000 – 2021 SitePoint Pty. Ltd.