Context
I have repos on GitHub, a self-hosted Forgejo, a Gitea box, and one GitLab team — four notification inboxes, and none of them know the others exist. In practice they got checked inconsistently, with the important thing always in the one I skipped.
git-notifications is a self-hosted dashboard that watches repositories across all four platforms and folds their activity into a single feed.
The hard part
Four APIs that describe the same concepts and agree on almost none of the details. Pagination works differently on each one. So does auth. Event payloads come back in different shapes, with different timestamps and different ideas about what even counts as an event. Gitea and Forgejo look identical until you hit the corners where the fork diverged, so they share one client with the differences handled inside it.
Most of the design work went into the provider layer: one interface that every forge answers the same way, so the poller, the feed, the push pipeline, and the browser extension never have to care where an event came from. The interface had to hide each API's quirks without dropping data in translation.
Approach
Everything downstream of the provider layer sees normalized events and nothing else. A background poller checks each tracked repo on its own interval, and you choose what to follow per repo instead of subscribing to a firehose.
Polling over webhooks was deliberate. Webhooks need every forge to reach your server, which is exactly what a self-hosted Gitea box behind someone's firewall can't do. Polling only needs outbound requests, so it works against any instance you hold a token for.
Delivery fans out from there: the in-app feed, Web Push straight to the browser, and a Chrome MV3 extension that runs against the app's API.
What shipped
9.4k LOC across the SvelteKit dashboard, the MV3 extension, a Go mirror service, and the Web Push pipeline, packaged and deployed as a Cloudron application.
Day to day it disappears. Add a forge URL and a token, pick what to watch, and the feed fills in on its own.