GitHub Webhook Listener → SQLite Logger → Custom Event Handlers
RepoLedger listens for GitHub webhooks at /webhook, verifies each request’s HMAC SHA-256 signature (X-Hub-Signature-256), stores the event in a local SQLite database (github_events.db), and dispatches a pluggable handler for each X-GitHub-Event. You can use this as a foundation for notifications, CI/CD triggers, dashboards, or any workflow that starts from GitHub events.
RepoLedger/
├── .env
├── .gitignore
├── package.json
├── README.md
└── src/
├── app.js
├── server.js
├── config/
│ ├── database.js
│ └── logger.js
├── routes/
│ ├── webhook.js
│ └── events.js
└── utils/
├── signatureValidator.js
└── eventHandler.js- .env Environment variables (PORT, WEBHOOK_SECRET)
- .gitignore Git ignores (node_modules/, github_events.db, logs/, etc.)
- package.json Dependencies and scripts
- README.md Documentation
Within src/:
- app.js Configures Express, raw-body parsing on
/webhook, and mounts/api/events/latest - server.js Initialises SQLite, then starts Express on the configured port
- config/logger.js Winston logger setup
- config/database.js Initialises (and migrates) SQLite and exports
insertEvent(...) - routes/webhook.js Verifies signature, logs to SQLite, then invokes the custom handler
- routes/events.js Provides
GET /api/events/latestto return the most recent logged event - utils/signatureValidator.js Validates GitHub’s
X-Hub-Signature-256 - utils/eventHandler.js Dispatches logic per GitHub event (e.g.
push,pull_request,issues)
The SQLite file (github_events.db) is created automatically at runtime in the project root.
- Node.js v16 or newer
- npm (bundled with Node.js)
- Git (to clone or manage the repo)
(No external database is required—everything runs locally with SQLite.)
-
Clone or copy the repository:
git clone https://github.com/Blindeenlightz/RepoLedger cd RepoLedger -
Install dependencies:
npm install
-
Create a
.envfile in the project root with:PORT=3000 WEBHOOK_SECRET=<your_github_webhook_secret>
PORT— Port on which the Express server will listen (default: 3000)WEBHOOK_SECRET— The exact “Secret” you set when configuring the GitHub webhook
-
Start the service:
npm start
You should see logs like:
2025-06-02 15:00:00 [info]: SQLite initialised at /path/to/repo/RepoLedger/github_events.db 2025-06-02 15:00:00 [info]: Server listening on port 3000
Because GitHub needs a public HTTPS endpoint for webhooks, you can use ngrok to expose your local server at a temporary public URL. This lets you test webhooks on your local machine.
-
In one terminal, ensure your Node server is running on port 3000:
npm start
-
In another terminal, start ngrok to tunnel port 3000:
ngrok http 3000
You should see something like:
Session Status online Version 3.22.1 Region United States (California) (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding https://abcd1234.ngrok-free.app → http://localhost:3000
-
Copy the HTTPS URL (e.g.
https://abcd1234.ngrok-free.app). -
In your GitHub repository settings → Settings → Webhooks → Add webhook, set:
- Payload URL:
https://abcd1234.ngrok-free.app/webhook - Content type:
application/json - Secret: exactly the same as
WEBHOOK_SECRETin your.env - Which events?: “Send me everything” or select specific events (
Push,Pull request,Issues, etc.) - Click Add webhook.
- Payload URL:
-
Trigger an event in your repo (push a commit, open a PR, etc.). ngrok’s console will show a
POST /webhookforwarding to your local server. If everything is set up correctly, your server console logs should include:Received POST /webhook Event inserted: type=push, at=2025-06-02T15:05:12.345Z Push in my-org/my-repo by octocat: commits abc123
If you deploy RepoLedger to a public server (or cloud VM) with a real domain (e.g. https://repoledger.example.com), configure GitHub’s Payload URL as https://repoledger.example.com/webhook and use the same WEBHOOK_SECRET.
- Signature Validation
- The Express route reads
req.bodyas a raw Buffer (configured bybody-parser.raw({ type: '*/*' })). - It extracts the
X-Hub-Signature-256header and computes an HMAC SHA-256 over the raw body usingWEBHOOK_SECRET. - If the computed hex digest matches the header’s value, the request is authentic; otherwise, it is rejected with a 400 status.
- The Express route reads
- JSON Parsing & Database Logging
- After signature validation, the raw Buffer is parsed into a JSON object.
insertEvent(eventType, payload)is called to store a record in SQLite, includingeventType, the full JSON‐stringified payload, and a timestamp (receivedAt).
- Event Dispatch
- Once logged,
handleEvent(eventType, payload)is invoked. It contains aswitchstatement that routes each event type to a custom handler function (e.g.handlePushEvent,handlePullRequestEvent,handleIssuesEvent). - By default, these handlers log details, but you can extend them to send emails, trigger CI pipelines, post to Slack, or perform other actions.
- Once logged,
- Immediate HTTP Response
- The route responds with
200 { "received": true }as soon as the event is safely inserted into the database. - Custom handler logic runs asynchronously in the background (errors are caught and logged but do not block the HTTP response).
- The route responds with
- Read-back Endpoint
- A GET request to
/api/events/latestreturns the most recently inserted event as JSON. This is useful for testing (e.g. via Postman) or building dashboards that show the latest GitHub activity.
- A GET request to
The SQLite database (github_events.db) contains one table:
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
eventType TEXT NOT NULL,
payload TEXT NOT NULL,
receivedAt TEXT NOT NULL
);id— Auto‐incrementing integereventType— GitHub event name (string)payload— Entire JSON payload, stored as a stringreceivedAt— UTC ISO timestamp of insertion
You can use the sqlite3 CLI to query events:
-
Enter the SQLite shell:
sqlite3 github_events.db
-
List stored events:
SELECT id, eventType, receivedAt FROM events;
-
View a specific payload (e.g.
id=1):SELECT payload FROM events WHERE id=1;
-
Exit:
.exit
Because the full JSON is in payload, you can even pipe it into jq for further inspection:
nginx
sqlite3 github_events.db "SELECT payload FROM events WHERE id=1;" | jq .
Open src/utils/eventHandler.js. By default it includes examples for:
- push: Logs repository name, pusher, and commit IDs.
- pull_request: Logs PR number, action, and author.
- issues: Logs issue number, action, and sender.
To add a new handler:
- Define an
async function handleMyEvent(payload) { ... }below the existing handlers. - Add a
case 'my_event_type': await handleMyEvent(payload); break;in theswitch(eventType)block.
For example, to handle a release event:
// In src/utils/eventHandler.js
export async function handleEvent(eventType, payload) {
switch (eventType) {
case 'push':
await handlePushEvent(payload);
break;
case 'pull_request':
await handlePullRequestEvent(payload);
break;
case 'issues':
await handleIssuesEvent(payload);
break;
case 'release':
await handleReleaseEvent(payload);
break;
default:
logger.info(`No handler for event type: ${eventType}`);
break;
}
}
async function handleReleaseEvent(payload) {
const action = payload.action; // e.g. "published", "created"
const tagName = payload.release.tag_name;
const repo = payload.repository.full_name;
const author = payload.sender.login;
logger.info(`Release ${tagName} in ${repo} was ${action} by ${author}`);
// …add your logic here… (e.g., notify team, deploy docs, etc.)
}You can call third‐party APIs, post to chat services, send emails, or trigger deployments from within these handlers.
- “Invalid signature” errors
- Ensure the GitHub webhook’s Secret exactly matches
WEBHOOK_SECRETin.env. - Verify GitHub is sending the
X-Hub-Signature-256header (older GitHub apps may sendX-Hub-Signaturewith SHA1 instead of SHA256). If needed, adjustsignatureValidator.jsto support SHA1.
- Ensure the GitHub webhook’s Secret exactly matches
- Database file not created
- Check write permissions in the project directory.
- Look at console logs:
SQLite initialised at /path/to/repo/RepoLedger/github_events.db.
- Handler code not running
- Confirm that
handleEventis imported and invoked inroutes/webhook.js. - Check console logs for any errors in the handler functions.
- Confirm that
- Port conflicts
- If port 3000 is in use, change
PORTin.envand restart.
- If port 3000 is in use, change
- ngrok ERR_NGROK_8012 (cannot connect to localhost:3000)
- Ensure your Node server is running and listening on port 3000 (
npm start). - Restart ngrok with
ngrok http 3000only after confirmingGET http://localhost:3000/healthreturns 200.
- Ensure your Node server is running and listening on port 3000 (
MIT License