A small Next.js app that receives GitHub webhooks and shows them as a live list in the browser. Clone it, run it on your computer, point one repo at it, and watch stars, issues, pushes, and other events appear.
This is a learning / playground project. Events are stored in memory (they disappear when you stop the server). It is not meant for production.
Note that this will only catch the GitHub events that happen while your server is running. In the "real world", you would have a server running that would be listening for these. This is just a test/demo app and we won't have the server running 24/7.
A webhook is GitHub calling you. Your app does not poll GitHub. When something happens on a repo, GitHub POSTs JSON to a URL you registered.
GitHub --POST--> public HTTPS URL (smee)
|
v
smee client on your computer
|
v
POST /api/webhooks/github (Next.js)
|
+-- verify HMAC signature (WEBHOOK_SECRET)
+-- ignore events from other repos
+-- store a short summary in memory
|
v
GET /api/events <-- the page keeps one SSE connection open
smee is a webhook relay for local development. GitHub will only POST to a public HTTPS URL; it cannot reach http://localhost:3000 on your computer. smee.io gives you a public channel URL to paste into GitHub, and npm run smee runs a client that forwards those payloads to Next.js. Without smee (or another tunnel), webhook deliveries never arrive.
The page also saves one owner/repo via POST /api/settings. Non-ping events are stored only if repository.full_name matches that pair (letter case is ignored). ping (GitHub’s “does this URL work?” event) is always stored.
You need Node.js 20.9+ and a GitHub repo you can add webhooks on.
git clone https://github.com/kevinsmithwebdev/wh-test.git
cd wh-test
npm install
cp .env.example .env.localOn Windows Command Prompt use copy .env.example .env.local instead of cp.
.env.local is gitignored. Variables:
| Variable | Purpose |
|---|---|
WEBHOOK_SECRET |
Shared secret. GitHub signs each POST; the app rejects unsigned or wrongly signed bodies with 401. Required by this app. |
SMEE_URL |
Your smee.io channel. Used by npm run smee, not by Next.js. |
The example value dev-secret is fine for local play. Use the same string in GitHub’s webhook Secret field. Change it if you want; then restart npm run dev.
smee channels are public: anyone with the URL can POST to it. Signature verification is what keeps random payloads out, so the GitHub Secret and WEBHOOK_SECRET must match.
You need two terminals.
Terminal 1 — the app
npm run devOpen http://localhost:3000. Enter the owner and repository name you will watch, then Save — do this before starring or pushing, or those events are ignored.
Terminal 2 — the tunnel
- Open https://smee.io and click Start a new channel. Copy the URL (
https://smee.io/...). - Each person needs their own channel. Do not reuse someone else’s.
- Put that URL in
.env.localasSMEE_URL.
npm run smeeLeave both processes running.
- On that repo: Settings → Webhooks → Add webhook (admin access required).
- Fill in:
| Field | Value |
|---|---|
| Payload URL | Your smee channel only (https://smee.io/...). Not localhost, and not /api/webhooks/github — smee already forwards to that path. |
| Content type | application/json |
| Secret | Same as WEBHOOK_SECRET in .env.local |
| SSL verification | Enable |
| Which events | Send me everything |
| Active | Checked |
- Click Add webhook. GitHub sends a
pingimmediately.
You should see a green delivery under Recent Deliveries, a log line in the smee terminal, and a Webhook ping card on the page.
If the secret was wrong, edit the webhook, set Secret correctly, Update webhook, then Recent Deliveries → ping → Redeliver.
Star the repo, open an issue, or push a commit. A new card should appear as soon as the webhook arrives.
If GitHub shows 200 but the page does not update, the payload was ignored: Watching owner/repo on the page must match repository.full_name in the delivery (letter case does not matter). The Next.js terminal logs ignored deliveries. Save the form again if you restarted the app (settings are in memory too).
With npm run dev running:
npm run example:pingThat sends a signed ping to http://127.0.0.1:3000/api/webhooks/github. You should get 200 {"ok":true} and a ping card. This avoids shell quoting issues when computing HMAC signatures.
| Path | Role |
|---|---|
app/page.tsx |
UI: repo form + event list |
app/api/settings/route.ts |
GET/POST the watched owner/repo |
app/api/events/route.ts |
GET SSE stream of stored events |
app/api/webhooks/github/route.ts |
POST receiver |
lib/github.ts |
Signature check + human-readable summaries |
lib/events.ts / lib/settings.ts |
In-memory stores |
hooks/useRepoSettings.ts / hooks/useEvents.ts |
Client state + live event stream |
scripts/load-env.mjs |
Shared .env.local loader for smee and example:ping |
.env.example |
Template for .env.local |
- One repo at a time (the one you type in the form).
- Events and settings vanish when Next.js restarts.
- The app only receives events while both Next.js and smee are running. GitHub will retry failed deliveries for a while, then stop.
- Do not deploy this as-is: serverless hosts do not share that in-memory store.