Skip to content

About

Self-hosted inbox for your GitHub release notifications: newest release per repo, hide rules, one-click unsubscribe and push notifications.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Releases: a GitHub release inbox

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.

The inbox: one entry per repository on the left, the selected release with its notes on the right

Screenshots

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.

The What's new tab listing the notes of three unread releases

Hide a component of a monorepo without unsubscribing. The pattern is suggested from the release, and the preview shows what it will hide.

The hide dialog with a suggested pattern and the matching releases

On a phone, swipe an entry left to mark it as read or right to unsubscribe; a toast offers Undo. Releases open full screen.

Swiping an entry to the left reveals Mark as read   A release opened on a phone

Settings for notifications and the inbox are synced across your devices; the display options apply to the device you set them on.

The settings panel with notifications, inbox and display options

Features

  • One entry per repository. Only the newest release is listed (+N older shows 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 older releases 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/k navigate, e marks as read, s snoozes, h hides, o opens 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.

Architecture

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

Behaviour worth knowing

  • 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 returns 304 and 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.

Desktop app

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 Run key 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.

Local development

Prerequisites: Python 3.12+ with uv, Node.js 22+.

  1. 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.
  2. 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
  3. Start the frontend at http://localhost:5173. It proxies /api to 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 build

Deployment on Ubuntu

The 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
  1. GitHub token. Create a classic personal access token at https://github.com/settings/tokens with the notifications and repo scopes. Fine-grained tokens can't access notifications.

  2. 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
  3. Configuration: copy the template and fill it in. The ghr commands 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.

  4. 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
  5. 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, HttpOnly and SameSite=Lax, cross-site requests that change data are rejected, and changing the password signs out every session.

  6. 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 ghr

The restart applies new database migrations before the app starts.

Configuration reference

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

About

Self-hosted inbox for your GitHub release notifications: newest release per repo, hide rules, one-click unsubscribe and push notifications.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages