Create a GitHub Tracker with Push Notifications in Svelte – SitePoint

Write powerful, clean and maintainable JavaScript.

RRP $11.95
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 supported in modern web browsers like Chrome by using something called service workers. Service workers are little pieces of JavaScript that run separately from the browser’s main thread, and as a result, can run offline if your app is installed as a PWA (progressive web application).
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 svelte. You can use TypeScript if you want. I’ll be using regular JavaScript.
Next, 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 App.svelte:
Your screen should now look like this:
The new tracker component
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.
Creating a new Atlas 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.
Connection string
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 vite with 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 import statements in JavaScript files. Instead, you need to use CommonJS require statements.
There’s one problem here. If you see the package.json of our app, you’ll notice that it has a line "type": "module". This means that each JavaScript file in the project is an EsModule. This is not what we want, so to mark all files in the 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:
Extract the 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.
The users collection in Atlas
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 fetch function:
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 App.svelte:
The above code will check the localStorage for a username and set isLoggedIn to 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 — track() and 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.

Create file api/trackrepo.js. This will be mapped to /api/trackrepo:
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 /api/untrackrepo:
The request body of this cloud function will be the same as that of the trackrepo function — the user’s username and the repo:
Next, here comes the code to delete the repository from the user’s trackedRepos:
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 track or untrack buttons are clicked:
And here’s how Tracker.svelte should look:
And here’s a screenshot of how the app should now appear.
Tracked repositories are accurate now
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.
Service workers are JavaScript files that can run in the background, off the browser’s main thread. This allows them to do stuff like run offline, run in the background and download large files. They’re mostly used for caching requests and for listening to events, both of which we will do.
To add a service worker, you need to add a JavaScript file that’s publicly available, like any CSS files. The name doesn’t really matter, but it’s usually named service-worker.js or sw.js. This file should be publicly served like your CSS, so put it in the public directory.
Service workers work by listening to events. For caching files, so your app works offline, you’ll listen to the install, activate and 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 onMount:
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 public directory.
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 install.
The older cache is deleted, so only the latest is saved to the user’s computer. This utilizes less storage. This happens during activate.
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 fetch.
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 /offline.html.
Screenshot of the application tab

Now all that’s left to do is to serve this file when there’s no network:
The 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:
The 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 public/icons.
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.
The new tracker component
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 default, denied and 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:

Remember to 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 VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY.
Vercel environment variables
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 web-push:
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 subscribe function:
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.
Push Notification Subscription logged in 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 api/storeendpoint.js:
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 App.svelte:
Refresh the page, and you should see that the current browser’s push endpoint and keys are stored in the MongoDB database in the subscription object.
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, false otherwise:
Since 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 method. 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:
Delete the fetched collection from MongoDB and call the cloud function again. You should now receive a notification from the web browser.
The notification
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.
Notification prompt
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.
Creating a new CRON job
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.
Learn the basics of programming with the web’s most popular language – JavaScript
A practical guide to leading radical innovation and growth.
© 2000 – 2021 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.