Clean GitHub notifications in Telegram.
Small Go service for repository events, subscriptions, and durable delivery.
Quick Start · Using Branchy · How It Works · Configuration · Security · Docs
GitHub notifications are useful, but raw webhook delivery becomes noisy fast: too many event types, duplicated formatting, fragile direct sends, and unclear group permissions.
Branchy keeps the MVP deliberately narrow:
| Need | Branchy approach |
|---|---|
| Focused event stream | Supports only push, pull_request, and release |
| Telegram-first setup | Uses /start plus inline buttons, with settings in DM |
| Safe group delivery | Enables groups only after admin or creator verification |
| Reliable sending | Stores notification jobs in PostgreSQL before delivery |
| Clean messages | Renders compact, sanitized Rich HTML with bounded GitHub media |
Result: one small service that turns GitHub activity into readable Telegram updates without expanding beyond the MVP.
| Channel | Version | Meaning |
|---|---|---|
| Latest | v1.2.0 |
Latest tag |
| Stable | v1.2.0 |
Latest non-prerelease tag |
The MVP has been live-tested with Telegram and GitHub. v1.2.0 is the current
stable release: Bot API 10.3 notification cards, in-message buttons, PR More
overlays, and disabled settings controls.
Branchy messages keep one event, one repository, and the useful links up front.
| Push | Pull request | Release |
|---|---|---|
|
2 new commits pushed to FreshLabDev/branchy · main · amtiYof2a07de fix Telegram layouta4e7f27 clarify release flowOpen compare |
#42 Add branch filters opened in FreshLabDev/branchy · feat → main · amtiYoDescription is rendered as a compact quote. Open pull request · More |
v0.1.0 Release in FreshLabDev/branchy · v0.1.0 · amtiYoRelease notes render from GitHub Markdown. Open release |
You need Docker, PostgreSQL, a Telegram bot token from BotFather, a GitHub OAuth App, and a public HTTPS URL for OAuth callbacks and webhooks.
# 1. Copy local configuration
cp .env.example .env
# 2. Fill the required secrets and public URL
$EDITOR .env
# 3. Start Branchy and PostgreSQL
docker compose up --buildCreate the GitHub OAuth App callback URL with the same public base URL:
${PUBLIC_BASE_URL}/oauth/github/callback
Branchy runs startup migrations from migrations/ and records completed
versions in schema_migrations. Keep AUTO_MIGRATE=true for local
development.
The bundled docker compose is for local development only. It runs a local
PostgreSQL and seeds a minimal shared core schema (deploy/core-init.sql) so
migrations that reference core.person / core.chat boot cleanly. In the
shared production deployment Branchy instead connects to the existing
core-postgres; use the production deployment procedure in
docs/releases.md.
All user setup is button-driven inside Telegram.
- Open the bot in DM and send
/start. - Connect GitHub through OAuth.
- Pick repositories and subscribe to
push,pull_request, orrelease. - Configure the selected events: branches for branch-based events, pull request actions, and release type.
- Choose DM delivery or an eligible Telegram group.
- View, pause, edit, delete, or test subscriptions from the inline menus.
About on the main menu reports the running version — the same string
/healthz returns — along with the supported events, the repository, and who
to reach.
Groups become available only after Branchy has seen the group. Before group
delivery is enabled, Branchy verifies that the Telegram user is a group
creator or administrator.
In groups, /start is registered as a Bot API 10.2 ephemeral command. Its DM
prompt is visible only to the user who invoked it; Branchy never posts a public
fallback into the group. That panel offers About and Close; Close removes
it from the chat.
Branchy is one Go service with PostgreSQL as its only durable store. Branchy's
own tables — subscriptions, the notification outbox, OAuth and runtime state —
live in a branchy schema. Telegram identity and presence (users and chats) are
delegated to a shared core schema (core.person, core.chat), which Branchy
upserts via core.touch before any dependent write. In production that schema
lives in the shared core-postgres database; local docker compose seeds a
minimal core schema so development boots the same way.
telegram poller -> inline-button UI
http server -> OAuth callback and GitHub webhooks
outbox worker -> Telegram delivery and retries
Webhook handling is intentionally fast:
verify signature -> dedupe delivery -> enqueue jobs -> return 200
Delivery happens outside the webhook request:
poll pending jobs with FOR UPDATE SKIP LOCKED
-> send Telegram
-> mark sent, retry, or failed
Temporary Telegram or GitHub failures retry with retry_at and attempts.
Permanent delivery failures are marked failed. Subscription changes that
cannot synchronize the repository webhook are compensated back to their prior
database state, followed by a best-effort restoration of the prior hook.
| Included | Excluded |
|---|---|
| GitHub OAuth through an OAuth App | GitHub App installation flow |
| Telegram DM and verified groups | Non-Telegram delivery channels |
push, pull_request, release |
Issues, comments, deployments, workflow runs |
| PostgreSQL outbox delivery | Direct sends inside webhook handlers |
/healthz and /metrics operational health |
Billing or paid plans |
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
yes | - | PostgreSQL connection string; Branchy's tables live in the branchy schema, so a shared database needs search_path=branchy (append options=-csearch_path%3Dbranchy) |
PUBLIC_BASE_URL |
yes | - | Public HTTPS base URL |
TELEGRAM_BOT_TOKEN |
yes | - | Bot token from BotFather |
GITHUB_CLIENT_ID |
yes | - | GitHub OAuth App client ID |
GITHUB_CLIENT_SECRET |
yes | - | GitHub OAuth App client secret |
GITHUB_WEBHOOK_SECRET |
yes | - | Secret for GitHub webhook signatures |
APP_SECRET |
yes | - | Token encryption secret, 32+ characters |
GITHUB_OAUTH_SCOPE |
no | repo read:user |
OAuth scopes |
HTTP_ADDR |
no | :8080 |
HTTP listen address |
MIGRATIONS_DIR |
no | migrations |
Migration directory |
AUTO_MIGRATE |
no | true |
Run migrations on startup |
OUTBOX_POLL_INTERVAL |
no | 2s |
Outbox poll interval (Go duration) |
OUTBOX_BATCH_SIZE |
no | 20 |
Max jobs claimed per poll |
OUTBOX_SEND_TIMEOUT |
no | 20s |
Per-message Telegram send timeout |
OUTBOX_LEASE |
no | 2m |
Job processing lease duration |
OUTBOX_RETENTION_DAYS |
no | 7 |
Days to keep terminal jobs and dedupe records |
NOTIFICATION_MAX_ATTEMPTS |
no | 5 |
Delivery attempts before a job is failed |
TELEGRAM_API_TIMEOUT |
no | 30s |
Per-request Telegram API timeout (long polling adds its own headroom) |
GITHUB_API_TIMEOUT |
no | 20s |
Per-request GitHub API timeout |
WEBHOOK_RATE_LIMIT |
no | 30 |
Webhook endpoint rate limit, requests per second |
WEBHOOK_RATE_BURST |
no | 60 |
Webhook endpoint burst allowance |
The default repo read:user scope is broad, but it supports private repository
visibility and repository webhook management through the OAuth App flow.
- GitHub OAuth tokens are encrypted at rest with AES-GCM using
APP_SECRET. - GitHub webhook signatures are verified over the raw body before JSON parsing.
- GitHub delivery IDs are treated as idempotency keys.
- OAuth state is single-use and expires.
- GitHub notifications use Telegram Rich Messages (
sendRichMessage). Branchy renders GitHub Markdown to allowlisted Rich HTML and strips unsafe or Telegram-specific tags, attributes, and URL schemes before delivery; bot UI keeps classic HTML parse mode. - Notification links are restricted to
http(s)URLs. - Logs avoid Telegram bot tokens, GitHub tokens, webhook secrets, OAuth client secrets, raw authorization headers, and full Telegram Bot API URLs.
Only two public routes are required:
/oauth/github/callback
/webhooks/github
Put HTTP_ADDR behind a TLS-terminating reverse proxy or tunnel and set
PUBLIC_BASE_URL to the matching HTTPS URL.
/healthz reports database status, Telegram polling freshness, worker
freshness, outbox counts, and the build version without exposing secrets.
/metrics exposes Prometheus counters (webhook deliveries, notification
outcomes, Telegram rate limits, automatic pauses). Both endpoints return counts
only; restrict them at your reverse proxy if you do not want them public.
deploy/ws04/compose.yaml is the production stack. It pulls the image the
release workflow published to GHCR and never builds one:
# in the stack directory, alongside .env
BRANCHY_IMAGE=ghcr.io/freshlabdev/branchy@sha256:<digest> docker compose up -dPin BRANCHY_IMAGE by digest rather than tag. A digest names one exact build,
so a rollback is a one-line change with nothing to rebuild, and docker inspect on the running container answers which commit it came from.
docker run --rm -v "$PWD":/src -w /src golang:1.26.5-alpine go test ./...
docker run --rm -v "$PWD":/src -w /src golang:1.26.5-alpine go vet ./...
docker compose config| Document | Purpose |
|---|---|
| Architecture | Service structure and core decisions |
| GitHub integration | OAuth, scopes, and repository webhooks |
| Telegram behavior | Bot interaction rules and group delivery |
| Versioning | Pre-release and stable version line |
| Release process | Changelog and GitHub Release rules |
Releases · Changelog · Apache-2.0 · NOTICE
Branchy is open source software by Asterfield.
Copyright 2026 Asterfield.