A self-hosted inbox for the GitHub releases you watch. It shows the newest release of every repository, with release notes, the README, AI summaries and one-click triage, and notifies you when something new ships.
What's new collects the notes of every unread release of a repository, here the newest release and its two older ones, in the light theme.
Hide a component of a monorepo without unsubscribing. The pattern is suggested from the release, and the preview shows what it will hide.
On a phone, swipe an entry left to mark it as read or right to unsubscribe; a toast offers Undo. Releases open full screen.
Settings for notifications and the inbox are synced across your devices; the display options apply to the device you set them on.
- One entry per repository. Only the newest release is listed (
+N oldershows the rest; switch versions in the detail view). What's new combines the notes of every unread release of a repository, so nothing between your last visit and the newest release is missed. - Inbox, Snoozed, Read and Hidden views. Every release is in exactly one of them. Actions
on an entry cover what it stands for: marking it as read moves the release and its
+N olderreleases in that view (within the current search) to Read. Read state is mirrored to github.com. - Snooze. Put a release aside until later today, tomorrow, the weekend or next week. It comes back to the inbox with a reminder notification.
- Breaking changes stand out. Releases whose notes mention breaking changes, or that are a new major version of a component, get a Breaking badge.
- Hide components. Hide releases of a single component of a repository, such as
web@*in a monorepo, without unsubscribing. Hidden releases stay available in the Hidden view. - Pre-releases on your terms. Treat them like other releases, keep them in the inbox without notifications, or keep them in the Hidden view.
- Read as you go. Optionally, a release is marked as read once you move on to another repository or close it.
- Unsubscribe in one click. Stops watching the repository on GitHub.
- Swipe to triage. In the inbox and snoozed views, swipe an entry left to mark it as read or right to unsubscribe. Both actions, whether you swipe, click or use a shortcut, show a toast with Undo for six seconds. The change is only sent to GitHub after that, so undoing leaves GitHub untouched. When you leave the page or switch apps, pending actions are sent right away.
- Copy a release link. Long-press an entry on a phone, or use the menu of a release (
c). - Download release files. Releases with attached files get a download button; the file for your device is suggested first. On phones the files open in a drawer from the bottom.
- AI summaries. Summarize a release, or everything that's new in a repository, with Claude. Summaries are cached and created only when you ask.
- Release notes and README rendered as GitHub-flavoured markdown, with working anchors, footnotes, light/dark logos, and relative images in READMEs of private repositories.
- Search across repositories, release names, tags and notes.
- Star counts next to each repository, refreshed daily.
- Notifications where you want them. Browser push, ntfy, Telegram and
the desktop app. Every repository you watch notifies you, including ones you start watching
later; mute a repository with the bell on one of its releases (or
m), or only hear about releases with breaking changes. - Live updates. GitHub is polled every minute (at the interval GitHub asks for), and Sync now polls right away. Open tabs update instantly via server-sent events. Recent releases are re-checked every 30 minutes, so edited notes show up, and a pre-release that is promoted to a stable release notifies you again.
- Keyboard driven.
j/knavigate,emarks as read,ssnoozes,hhides,oopens on GitHub,/searches, and?lists all shortcuts. - Display options per device. Light, dark or system theme, a compact list, star counts, relative or calendar dates, and the tab releases open on.
- Installable as an app on desktop and phones, with the number of inbox entries on its icon. The layout adapts from phones to wide screens.
- Desktop app for Windows, macOS and Linux. It shows your server's app in a native window, keeps running in the system tray when you close it, can start at login and shows notifications natively, with buttons to mark as read, snooze or mute. See Desktop app.
frontend/ React 19 + TypeScript + Vite, shadcn/ui (Radix, Tailwind CSS v4), TanStack Query
backend/ FastAPI, SQLAlchemy 2 (async) on SQLite, Alembic, httpx, pywebpush, Anthropic SDK
deploy/ systemd units (service and daily backup), nginx site and environment template
desktop/ Tauri 2 desktop app (Rust): native window, tray and notifications around the web app
website/ Project website (Next.js 16, shadcn/ui), exported statically to GitHub Pages
In production nginx serves the built frontend and proxies /api to a single uvicorn process.
That process also runs the poller, so run exactly one instance.
| Backend module | Responsibility |
|---|---|
ghr.domain |
Views, pre-release modes and notification choices |
ghr.github |
Typed client for the GitHub REST API |
ghr.services.sync |
Imports release notifications and refreshes recent releases |
ghr.services.filters |
SQL conditions for the views, hiding and search |
ghr.services.releases |
Read-side queries: views, counts, search, history, what's new |
ghr.services.inbox |
Read state, snoozing and unsubscribing |
ghr.services.breaking |
Breaking-change detection |
ghr.services.hide_rules |
Hide rules and their previews |
ghr.services.notifications |
Web Push, ntfy, Telegram, desktop app, and what is announced |
ghr.services.snooze |
Ends snoozes and sends reminders |
ghr.services.stars |
Star counts of repositories, refreshed daily |
ghr.services.assets |
Files attached to releases |
ghr.services.preferences |
Preferences: pre-releases, notifications, read behaviour |
ghr.services.summaries |
Summaries and their cache; claude creates them |
ghr.services.readme |
README cache with conditional requests |
ghr.scheduler |
Background loops: sync, refresh, snoozes, rate-limit backoff |
ghr.migrations |
Alembic migrations, applied by ghr migrate |
ghr.api |
HTTP routes; everything except sign-in requires a session |
| Frontend directory | Responsibility |
|---|---|
src/app |
Providers, routing, header and app-wide error handling |
src/features/<feature> |
One area each: its API hooks (api.ts), components and logic |
src/features/releases |
The inbox: list, detail, actions with undo, keyboard shortcuts |
src/components |
Shared components; ui/ holds the shadcn/ui components |
src/features/settings |
The settings panel (a full page on phones) |
src/hooks |
Generic hooks: media queries, gestures, hotkeys, app badge |
src/lib |
API client, display settings, hotkeys, time and markdown URLs |
- Data source. The app reads your GitHub notifications
and keeps the ones about releases, so it shows exactly the releases you subscribed to.
Polling uses
If-Modified-Since, so a poll that finds nothing new returns304and doesn't count against your rate limit. When GitHub asks to slow down, the app waits as long as asked. - Robust sync. A single release that is deleted or no longer accessible (for example because of SSO enforcement) is skipped instead of blocking the sync. A release is only removed when GitHub reports it gone while its repository is still reachable. Renamed repositories are followed. Progress of a long first import is kept if it is interrupted.
- Read state. The app is the source of truth. Releases that were already read on GitHub are imported as read. Marking as read also marks the GitHub thread as read. GitHub has no API to mark a thread unread, so Mark as unread only affects this app.
- Unsubscribe. GitHub's API can't switch a repository to "releases only", so unsubscribing stops watching the repository entirely. If you watch it with Custom → Releases, that's exactly the same thing. If you start watching it again on GitHub, its next release brings it back.
- Hide rules. Glob patterns (
*,?,[...]) are matched case-insensitively against the release name and tag of one repository. New releases that match are hidden as well. A hidden release names the rule that hides it, with Show again to remove the rule. - Browser push needs HTTPS. On iOS, add the site to the home screen first (Safari → Share → Add to Home Screen), then turn it on in the app's settings. ntfy and Telegram work anywhere.
- The count on the app icon needs the app installed (Chrome or Edge: Install app; Safari: Add to Dock or Home Screen). While the app is closed, browser push keeps it up to date; on iOS it only shows once notifications are allowed.
- Summaries send the release notes to the Anthropic API and are billed to your API key.
The desktop app is a native window around the web app of your server, built with Tauri 2. It uses the system's web view (WebView2 on Windows, WebKit on macOS and Linux), so it doesn't bundle a browser. It needs a server of the same version or newer: older servers don't send it notifications.
- On first start it asks for your server's address; you then sign in as usual. Change server… in the tray menu asks again. A server that redirects http to https is followed to https.
- Closing the window keeps the app in the system tray. A single left click on the tray icon shows a hidden or minimized window, or hides an open one. On macOS the Dock icon also reopens it. Right-click the tray icon for Open Releases, Sync now, Reload (for example after the server was unreachable) and Quit. A dot on the tray icon and on the taskbar button (a count on the macOS Dock) shows that the inbox has entries.
- Show only in tray hides the Dock icon on macOS or the taskbar button on Windows and Linux, even while the window is open. Use the tray icon or Open Releases in its menu to bring the window back. The choice applies immediately and is saved for the next launch; uncheck it to restore the Dock/taskbar icon. Linux desktop environments may ignore this taskbar hint, and need system tray support to keep the app accessible.
- On macOS and Windows, Monochrome icon in the tray menu switches to a rocket that follows the menu bar or taskbar's light or dark appearance, including its unread dot. Windows uses the system/taskbar colour mode, even when apps use a different mode, and updates while the app is running or hidden. The choice is saved for the next launch; the Dock/taskbar app icon stays coloured.
- Start at login in the tray menu adds the app to the system's login items (the registry's
Runkey on Windows, a Launch Agent on macOS, an autostart entry on Linux). Started that way, it waits in the tray with the window hidden (--hidden) and still notifies you. - Notifications arrive over the app's live-update connection (
/api/events?notifications=true) and follow the same rules as the other channels: mutes, pre-releases, Breaking only. They need no setup on the server, but only reach a desktop app that is running; there's no queue for one that is offline. Clicking a notification opens its release. - They also need the server's app in the window to be signed in and connected. The tray's tooltip and the first item of its menu show when it isn't: Not connected (the server wasn't reachable, the connection dropped, or the setup page is open) or Sign in to get notifications (the session ended). A page that doesn't report within 30 seconds of loading, such as the web view's error page, counts as not connected. If that lasts 30 seconds while the window is hidden, one notification says so; the next only after it was connected again.
- Notifications about releases have buttons: Mark as read, Snooze until tomorrow (9:00) and Mute repository. They act on exactly the releases the notification is about; one about releases of several repositories has no Mute. The web app does the work with your session (the desktop never calls the server itself) and applies it right away, with Undo in the app's toast. A failure is shown as a notification while the window isn't in front. On Windows the buttons only work while the notification is on screen, not from the Action Center; macOS shows them in an Options menu.
- Links that leave the app, such as releases on github.com, open in your browser. Only pages of the server you entered can use the desktop functions.
| Desktop module | Responsibility |
|---|---|
desktop/setup |
The page that asks for the server's address |
desktop/src-tauri/src/window.rs |
The main window, close to tray, links, the server's rights |
desktop/src-tauri/src/tray.rs |
Tray icon, its menu, the inbox dot and the connection state |
desktop/src-tauri/src/notifications.rs |
Showing notifications, clicks and their buttons |
desktop/src-tauri/src/status.rs |
Whether notifications arrive; telling once when they don't |
desktop/src-tauri/src/commands.rs |
What the pages can call: connect, notify, inbox count, state |
desktop/src-tauri/src/config.rs |
The app's settings (server address) in its config folder |
frontend/src/lib/desktop.ts |
The web app's side: detects the desktop app and talks to it |
Building. Install Rust and the Tauri prerequisites for your platform (on Windows, the MSI installer also needs the VBSCRIPT optional feature), then:
cd desktop && npm install
npm run dev # run it; for local development, connect to http://localhost:5173
npm run build # installers in src-tauri/target/release/bundle/The app's version is the one in desktop/src-tauri/Cargo.toml. For every push to main,
GitHub Actions builds the installers for Windows (.msi, .exe), macOS (one universal .dmg
for Apple silicon and Intel) and Linux (.AppImage, .deb, .rpm) and keeps them as artifacts
of the workflow run.
Publishing a GitHub release or prerelease also rebuilds the installers from its tagged commit.
Once all three platforms pass their checks and builds, the workflow attaches the installers
to that release's Assets. Before tagging, update the desktop version in Cargo.toml and
Cargo.lock; the release tag must match it, with an optional v prefix (for example v0.5.2).
Saving a draft does not start a build. A failed run can be rerun from Actions; uploads replace
assets with matching filenames and leave other release attachments alone.
The builds are not signed: Windows SmartScreen and macOS Gatekeeper warn before the first start.
Prerequisites: Python 3.12+ with uv, Node.js 22+.
-
Create
backend/.env:GHR_GITHUB_TOKEN=<token, for example from `gh auth token`> GHR_USERNAME=admin GHR_PASSWORD_HASH='<output of: uv run ghr hash-password>' GHR_SESSION_SECRET=<output of: uv run ghr generate-secret> GHR_SECURE_COOKIES=false GHR_ENABLE_API_DOCS=true # Optional: see deploy/ghr.env.example for push, ntfy, Telegram and summaries.
-
Start the backend, which listens on port 8000 (API docs at http://localhost:8000/api/docs):
cd backend && uv sync && uv run ghr migrate && uv run ghr serve --reload
-
Start the frontend at http://localhost:5173. It proxies
/apito the backend:cd frontend && npm install && npm run dev
Checks (also run by GitHub Actions on every push):
cd backend && uv run pytest && uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run deptry src
cd frontend && npm test && npm run typecheck && npm run lint && npm run format:check && npm run buildThe steps assume Ubuntu 24.04, the domain releases.example.com and the code in /opt/ghr.
uv uses the system's Python 3.12. Ubuntu's Node.js is too old to build the frontend, so install
Node.js 22 from NodeSource:
sudo apt install nginx certbot python3-certbot-nginx
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
sudo apt install nodejs-
GitHub token. Create a classic personal access token at https://github.com/settings/tokens with the
notificationsandreposcopes. Fine-grained tokens can't access notifications. -
User and code
sudo useradd --system --home /opt/ghr --shell /usr/sbin/nologin ghr sudo git clone https://github.com/Endogen/release-inbox /opt/ghr curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR=/usr/local/bin sh cd /opt/ghr/backend && sudo uv sync --frozen --no-dev cd /opt/ghr/frontend && sudo npm ci && sudo npm run build
-
Configuration: copy the template and fill it in. The
ghrcommands print the values for you.sudo install -d -m 0750 -o root -g ghr /etc/ghr sudo install -m 0640 -o root -g ghr /opt/ghr/deploy/ghr.env.example /etc/ghr/ghr.env cd /opt/ghr/backend sudo .venv/bin/ghr hash-password # GHR_PASSWORD_HASH (keep the single quotes) sudo .venv/bin/ghr generate-secret # GHR_SESSION_SECRET sudo .venv/bin/ghr generate-vapid-keys # GHR_VAPID_PUBLIC_KEY / GHR_VAPID_PRIVATE_KEY sudo nano /etc/ghr/ghr.env
Keeping the tokens in this root-owned file, rather than in a shell profile, keeps them out of shell history and away from other users. ntfy, Telegram and summaries are optional; leave their variables empty to turn them off.
-
Service: database migrations run automatically before every start.
sudo cp /opt/ghr/deploy/ghr.service /etc/systemd/system/ sudo systemctl daemon-reload && sudo systemctl enable --now ghr journalctl -u ghr -f
-
nginx and TLS
sudo cp /opt/ghr/deploy/nginx/snippets/*.conf /etc/nginx/snippets/ sudo cp /opt/ghr/deploy/nginx/ghr.conf /etc/nginx/sites-available/ghr sudo sed -i 's/releases.example.com/<your domain>/g' /etc/nginx/sites-available/ghr sudo certbot certonly --nginx -d <your domain> sudo ln -s /etc/nginx/sites-available/ghr /etc/nginx/sites-enabled/ghr sudo nginx -t && sudo systemctl reload nginx
Sign-in attempts are rate-limited by nginx and by the app. Cookies are
Secure,HttpOnlyandSameSite=Lax, cross-site requests that change data are rejected, and changing the password signs out every session. -
Daily backups: keeps the last 14 copies in
/var/backups/ghr.sudo install -d -m 0750 -o ghr -g ghr /var/backups/ghr sudo cp /opt/ghr/deploy/ghr-backup.service /opt/ghr/deploy/ghr-backup.timer /etc/systemd/system/ sudo systemctl daemon-reload && sudo systemctl enable --now ghr-backup.timer
To restore, stop the service, replace the database (removing the write-ahead log of the old one) and start it again:
sudo systemctl stop ghr sudo rm -f /var/lib/ghr/ghr.db-wal /var/lib/ghr/ghr.db-shm sudo install -m 0640 -o ghr -g ghr /var/backups/ghr/<backup>.db /var/lib/ghr/ghr.db sudo systemctl start ghr
Updating
cd /opt/ghr && sudo git pull
cd backend && sudo uv sync --frozen --no-dev
cd ../frontend && sudo npm ci && sudo npm run build
sudo systemctl restart ghrThe restart applies new database migrations before the app starts.
All settings are environment variables with the GHR_ prefix.
| Variable | Default | Description |
|---|---|---|
GHR_GITHUB_TOKEN |
required | Classic token with notifications and repo |
GHR_USERNAME |
required | Sign-in name |
GHR_PASSWORD_HASH |
required | Argon2 hash from ghr hash-password |
GHR_SESSION_SECRET |
required | At least 32 characters, from ghr generate-secret |
GHR_SESSION_MAX_AGE_DAYS |
30 |
Sign-ins end after this many days without a visit |
GHR_SECURE_COOKIES |
true |
Set to false only for local HTTP development |
GHR_LOGIN_MAX_FAILURES |
10 |
Failed sign-ins per address before it is blocked |
GHR_LOGIN_WINDOW_SECONDS |
900 |
Time window for the sign-in limit |
GHR_DATABASE_PATH |
data/ghr.db |
SQLite database file |
GHR_PUBLIC_URL |
unset | Address of the app, for links in ntfy and Telegram |
GHR_ENABLE_API_DOCS |
false |
Serve the interactive API docs at /api/docs |
GHR_POLL_INTERVAL_SECONDS |
60 |
Minimum poll interval (GitHub may ask for more) |
GHR_RELEASE_REFRESH_INTERVAL_SECONDS |
1800 |
How often recent releases are re-checked |
GHR_RELEASE_REFRESH_DAYS |
14 |
Releases published within this many days are checked |
GHR_README_CACHE_SECONDS |
3600 |
How long READMEs are served from the cache |
GHR_VAPID_PUBLIC_KEY |
unset | Browser push, together with the private key |
GHR_VAPID_PRIVATE_KEY |
unset | From ghr generate-vapid-keys |
GHR_VAPID_SUBJECT |
mailto:admin@localhost |
Contact address sent to push services |
GHR_NTFY_URL |
unset | ntfy topic URL, e.g. https://ntfy.sh/your-topic |
GHR_NTFY_TOKEN |
unset | Access token for protected ntfy topics |
GHR_TELEGRAM_BOT_TOKEN |
unset | Bot token from @BotFather |
GHR_TELEGRAM_CHAT_ID |
unset | Chat that receives the messages |
GHR_ANTHROPIC_API_KEY |
unset | Enables AI summaries |
GHR_ANTHROPIC_MODEL |
claude-opus-5-5 |
Model used for summaries |





