Recently, I became curious about how GitHub webhooks work. To deepen my understanding, I built a small Node.js project that sends an email notification every time a pull request (PR) is opened.
Yes, GitHub already has a built-in notification system, but building your own gives you full control and a deeper understanding of the ecosystem.
๐ First, Whatโs HMAC?
Before we dive into code, itโs important to understand how GitHub ensures that webhook events are secure.
HMAC stands for Hash-based Message Authentication Code. Itโs a cryptographic technique used to verify both:
- The integrity of a message (that it hasnโt been tampered with)
- The authenticity of the sender (that itโs really from GitHub)
GitHub does this by hashing the body of the request with a shared secret you provide when creating the webhook. It then sends that signature along with the request using the X-Hub-Signature-256
header.
๐งฉ Webhook Levels
Webhooks can be configured at three levels:
- Repository level โ scoped to a single repo
- Organization level โ applies to all repositories within an organization
- GitHub App level โ used in GitHub Apps for deep integration across multiple repositories or organizations
For this example, I used an organization-level webhook so it applies to all repos in my org.
๐ Signature Verification
To verify that the incoming request is really from GitHub, we need to:
- Capture the raw request body before itโs parsed by Express.
- Recompute the HMAC signature using the same secret.
- Use a constant-time comparison to prevent timing attacks.
Hereโs how I do it:
๐ธ Express Middleware
We add a raw body parser to capture the exact payload:
app.use(express.json({ verify: (req, _, buf) => { req.rawBody = buf; } }));
This step is critical โ HMAC must be calculated over the raw payload. If you parse it first, youโll get a mismatch.
๐ธ Signature Verifier (utils/verifier.js
)
import crypto from 'crypto'; import { config } from './config.js'; export const verifySignature = (req)=>{ const signature = req.headers['x-hub-signature-256']; if(!signature){ return false; } const hmac = crypto.createHmac("sha-256", config.GIT_WEBHOOK_SECRET ); hmac.update(req.rawBody); const expectedSignature = `sha256=${hmac.digest('hex')}`; return signature === expectedSignature; }
๐ฌ Sending Email Notifications
Once the signature is verified, I check if the action is "opened"
on a PR, and then send an email.
I used Nodemailer with Gmail as the SMTP service. Since my Gmail account uses 2FA, I generated an App Password to authenticate.Use the app password without spaces.
๐ธ Mailer (services/emailService.js
)
const transporter = nodemailer.createTransport({ service:"Gmail", auth:{ user: config.EMAIL, pass: config.PASSWORD } }) export const sendPRrequestMail = (recipents, subject, text)=>{ const mailOption = { from :config.EMAIL, to: recipents, subject: subject, text: text } return new Promise((resolve, reject)=>{ transporter.sendMail(mailOption, (error, result)=>{ if(error){ reject(error); }else{ resolve(result); } }) }) }
๐ Exposing the Server for GitHub to Reach
To allow GitHub to reach my local server, I had two options:
- Use a tunneling service like Ngrok
- Deploy to a cloud provider. I chose to deploy to Render, which has a generous free tier and makes deployment super easy. Once deployed, I used the Render URL as the webhook endpoint in GitHub. ---
โ Summary
- ๐ We used HMAC with a shared secret to verify webhook authenticity.
- ๐ฆ We used Nodemailer + Gmail to send email notifications.
- ๐ We deployed our app to Render to make it accessible to GitHub.
- ๐ง And we learned that understanding webhooks at a low level is a great way to grow as a developer.
๐ Full Source Code
You can view the complete code for this project on GitHub:
๐ https://github.com/Mehakb78/git-prrequest-mailer
Feel free to clone it, experiment!
Top comments (0)