Back to blog

2026-08-03

Real-Time GitHub Notifications with Server-Sent Events

How PulseRepo delivers live GitHub and deploy events to a dashboard and Telegram using SSE and an in-memory EventBus, no WebSockets or Redis required (yet).

We wanted a live feed of GitHub activity, pushes, PRs, deploys, showing up on a dashboard the instant it happened, plus the same events forwarded to Telegram. No polling, no "refresh to see if anything changed."

Here's how PulseRepo does it, and why we picked Server-Sent Events over the more obvious choice, WebSockets.

Why SSE and not WebSockets

WebSockets are bidirectional. The client can push messages back to the server. We didn't need that. Every event in PulseRepo starts in one place, an inbound webhook from GitHub, Vercel, Netlify, or Amplify, and flows one direction: server to client. Building bidirectional infrastructure for a one-way problem felt like the wrong trade.

SSE covers that for a fraction of the complexity. It's plain HTTP, so there's no protocol upgrade, no extra proxy configuration, none of the load-balancer headaches WebSockets can bring. The browser's EventSource API reconnects on its own; if a laptop sleeps or a connection drops, the client picks back up without any code from us. And it fits Next.js route handlers naturally: a streaming Response is all the server side needs.

If we ever need the client to talk back in real time, we'll reach for WebSockets then. Right now the product only moves data one way, so that's the only infrastructure we built.

The shape of the pipeline

Every event goes through the same four stages. GitHub, or Vercel, Netlify, Amplify, POSTs to src/app/api/webhook/[repoId]. The handler checks the request's HMAC signature against that repo's stored webhookSecret using crypto.timingSafeEqual, so the comparison itself can't leak timing information, and a repo that isn't in our database gets a 404 instead of a free pass to process an arbitrary payload.

From there, github-webhook-parser.ts normalizes whatever provider sent it into one internal ParsedEvent shape, so a pull request and a deployment look identical to everything downstream. Before delivery, we check EventSubscription.enabled for that repo and event type, since users choose exactly which events they want and the webhook handler is the one gate everything passes through. Finally the event fans out to two places: the in-memory EventBus, which any open dashboard connection is subscribed to, and the notification dispatcher, which hands it to a provider adapter, currently just Telegram.

Keeping notification providers swappable

The dispatcher has no idea what "Telegram" is. It only knows about a NotificationAdapter interface, send(event, destination), and a switch statement mapping each DeliveryChannel to an adapter. Adding WhatsApp or Slack later means one new adapter file and one new case. The webhook handler, the parser, the filtering logic, none of it changes.

That boundary mattered more than I expected going in. Notification code has a habit of growing provider-specific if branches everywhere unless someone draws the line early and defends it.

The EventBus is in-memory, on purpose, for now

EventBus today is a plain in-memory pub/sub. An SSE connection subscribes, a webhook publishes, done. That's a deliberate limit, not something we haven't gotten around to fixing. Keeping it in memory means a single Next.js instance never does a database round trip or talks to a broker just to move an event from webhook to browser, which keeps that path about as fast as it can be.

The interface itself doesn't assume a single process: publish(channel, event) and subscribe(channel, handler) would work the same way against Redis. Once we run more than one server, and we will, swapping the in-memory implementation for a Redis pub/sub adapter behind that same interface should be a contained change rather than a rewrite.

What I'd do differently

A few things worth knowing before you're three webhook providers deep. Build the ParsedEvent normalization layer before you add a second provider, not after; retrofitting a common shape once provider-specific fields have leaked into three other files is real work you can avoid. Pin down the EventBus interface early even while the implementation stays simple, since swapping an implementation behind a stable interface is easy and discovering the interface was never really defined is not. And treat webhook signature verification as the first thing you build rather than something you bolt on before launch. It shapes how the rest of the handler fails, whether that's a 404, a 401, or a silent 200 that just drops the payload.

If you're building anything that turns third-party webhooks into a live feed, give SSE a real look before reaching for WebSockets. Most webhook-driven products only move in one direction, and the simpler transport is usually the right call.


PulseRepo turns GitHub and deploy webhooks into filtered, real-time Telegram notifications. Try it free, or read why we chose Telegram over Slack for delivery.