Previously, we managed to . As you can remember, we get the status of deployment by processing script execution results. So let's use it to send a notification to ourselves each time we deploy our code. make our app redeploy on each commit For that matter, we will create another Node.js server app on our VPS. You can extend the app that we created for continuous deployment, although I would not recommend that. Instead, we can do that Unix way, where each app does its job and does it well. Additionally, we can use the reporting server to notify us about deployments of other parts of our app, e.g. frontend. As you can guess, we would need to implement both the client and server sides of the app. First, we will need a client (our CD server) that will send request upon successful (or failed) deployment. Second, we will make a server that will listen to those requests and send it further to the message provider of choice. Speaking of what service to use to send those messages, that is 100% up to. Personally, I use Telegram bot to deliver messages back to me, so I will use that as an example, but this design allows using any method to deliver messages, like SMS, email, Slack, or other. Client As with the CD server, we will need a way to check source authenticity. This time, we will use JWT, or JSON Web Tokens to sign our messages. Also, this time we will implement both signing and verification. Let's start by writing two helper functions that will deal with JWT generation. { paramString = .stringify(params); hash = crypto .createHash( ) .update(paramString) .digest( ); hash; } { payload = { : , : hash, }; token = jwt.sign(payload, secret); token; } ( ) function _getHash params const JSON const 'sha256' 'hex' return ( ) function _sign hash, secret const iss 'server' sha256 const return Here, creates a SHA256 hash of a message body, and , well, signs it using a secret. Let's use it in our client. _getHash _sign axios = ( ); crypto = ( ); jwt = ( ); client = axios.create({ : , }); { params = { : , app, } secret = process.env.SECRET; hash = _getHash(params); token = _sign(hash, secret); client.post( , params, { : { : token, }, }); } const require 'axios' const require 'crypto' const require 'jsonwebtoken' const baseURL 'https://our.reporting.server.url' async ( ) function sendSuccess app const success true const const const await '/cd/server' headers 'X-Signature' Here, we get our secret from file, use it to sign a request body, and then send it to our reporting server. .env Few things to note: the URL at which the reporting server is located, replace with yours. our.reporting.server.url endpoint which we send request to; I use as I have other sources like Netlify to receive updates from, but you can use anything, including . /cd/server / header: again, it can be almost anything, but I would suggest sticking to something similar as this is kinda standard. X-Signature And that's our client. Let's look at the server now. Server Again, we start with a helper function. { (signature == ) { ; } decoded; { decoded = jwt.verify(signature, secret); } (e) { ; } dataString = .stringify(data); hash = crypto .createHash( ) .update(dataString) .digest( ); hashMatches = decoded.sha256 == hash; issuerMatches = decoded.iss == issuer; (!hashMatches || !issuerMatches) { ; } ; } ( ) function checkSignature data, signature, secret, issuer if undefined return false let try catch return false const JSON const 'sha256' 'hex' const const if return false return true Similar to the one in the article on the CD server, this function validates that the signature is authentic. checkSignature Here's the rest of the server code. crypto = ( ); jwt = ( ); app.post( , (req, res) { data = req.body; signature = req.header( ); secret = process.env.SERVER_SECRET; issuer = ; (!checkSignature(data, signature, secret, issuer)) { res.status( ).end(); } success = data.success; app = data.app; error = data.error; bot.cd( , app, success); res.send( ); }); const require 'crypto' const require 'jsonwebtoken' '/cd/server' async const const 'X-Signature' const const 'server' if 403 const const const 'Server' 'Hello server!' What we do here is check the signature and send a message. A message is sent via the provider of your choice. Here, it's Telegram bot ( ). bot.cd('Server', app, success); Bonus: Netlify As another example, let's try sending a message each time our frontend updated on Netlify. Now, Netlify obviously doesn't need to hit our CD server, as it does CD itself. Instead, the Netlify webhook will go straight into our reporting server. Thankfully, here we can reuse most of the code that we wrote before (Netlify uses JWT to sign webhook requests). app.post( , (req, res) { data = req.body; signature = req.header( ); secret = process.env.NETLIFY_SECRET; issuer = ; (!checkSignature(data, signature, secret, issuer)) { res.status( ).end(); } success = data.state == ; app = data.name; bot.cd( , app, success); res.send( ); }); '/cd/netlify' async const const 'X-Webhook-Signature' const const 'netlify' if 403 const 'ready' const 'Netlify' 'Hello Netlify!' Here, we extract the signature from a header, match it with our locally stored key, and send a message if the signature is valid. : and don't to be different, but I highly recommend making them so. Otherwise, if one key is leaked (say, by a hacker attack on Netlify), another one will be compromised as well, making your stack less secure. Note NETLIFY_SECRET SERVER_SECRET have To add webhooks on Netlify, open a project, then click , then press . You can add webhooks for successful or failed builds among other events. Settings -> Build & Deploy -> Deploy notifications Add notification -> Outgoing webhook Bonus 2: Error handling Okay, I know you're tired by now, but there's another exciting thing I want to share with you: error logging. In other words, it will allow you to be notified each time you have an error in your app. In essence, it's very similar to sending a request from the CD server, only this time we will send the error. In your Node.js app, add custom error handler: { (process.env.ENV == ) { .log(err); } (process.env.ENV == ) { _sendRuntimeFailure(err.toString()); } next(err); } { app = ; params = { app, error, }; hash = _getHash(params); secret = process.env.SECRET; token = _sign(hash, secret); client.post( , params, { : { : token, }, }); } ( ) function errorWatcher err, req, res, next if 'dev' console if 'prod' async ( ) function _sendRuntimeFailure error const 'my-app' const const const const await '/runtime' headers 'X-Signature' Functions and are the same as we used above. We also use to set the ENV variable to or . That way, only production errors will be sent to you. _getHash _sign .env dev prod The only thing left is to tell express about our handler. app.use(errorWatcher); We will also need to wrap our async routes to make sure the error is passed to our handler. app.get( , wrapAsync(router.endpoint)); { { fn(req, res, next).catch(next); }; } '/endpoint' // Helper function to pass error down the middleware chain ( ) function wrapAsync fn return ( ) function req, res, next That's it. On the reporting server side, it's 100% identical to what we used for CD server and Netlify: get signature, verify it, and send a message is signature is valid. Wrapping up Here, we created another microserver, this time for reporting. The server collects events from multiple sources and routes them into a single place, e.g. Telegram. We managed to send events based on our CD server, Netlify, and express.js app's error handler.