Branchy is one Go service with three runtime surfaces:
- Telegram long polling for user interaction.
- HTTP routes for GitHub OAuth and GitHub repository webhooks.
- A notification outbox worker that sends queued Telegram messages.
The service stores its durable state in PostgreSQL. Branchy's own tables live in
a branchy schema (subscriptions, repositories, hooks, the notification outbox,
OAuth and runtime state), and Telegram identity/presence is delegated to a shared
core schema (core.person, core.chat) that Branchy upserts through the
SECURITY DEFINER core.touch function before any dependent write. In production
this is the shared core-postgres database; local docker compose seeds a
minimal core schema (deploy/core-init.sql) so migrations boot. GitHub webhook
handlers do not send Telegram messages directly; they create durable
notification_jobs rows and return quickly. The worker polls pending jobs with
FOR UPDATE SKIP LOCKED, sends Telegram messages, and updates job status.
cmd/branchy: process wiring and graceful shutdown.internal/config: environment parsing and required setting checks.internal/db: PostgreSQL store methods and data types.internal/github: small GitHub OAuth, REST, repository, branch, and webhook client.internal/oauth: OAuth state, PKCE, callback handling, and token encryption.internal/bot: the Telegram bot -- polling loop, menus, inline-button flows.internal/telegram: a thin adapter over the sharedgithub.com/FreshLabDev/tgclient, holding only Branchy's own send shapes.internal/subscriptions: subscription mutation and repository webhook synchronization.internal/webhooks: GitHub webhook signature validation, event parsing, filtering, dedupe, and outbox enqueue.internal/outbox: durable Telegram notification job worker and retry classification.internal/db: PostgreSQL store methods and startup migration runner.internal/notify: GitHub event notification formatting.
- A user sends
/startto the bot in DM. - The bot renders an inline main menu and creates a short-lived GitHub OAuth state for the connect button.
- GitHub redirects to
/oauth/github/callback. - Branchy exchanges the OAuth code, encrypts the access token, stores the connection, and sends a Telegram confirmation.
- The user creates a subscription in DM with inline buttons.
- Branchy creates or reuses one repository webhook for the selected repository.
- GitHub posts events to
/webhooks/github. - Branchy validates the signature, dedupes the GitHub delivery, parses the
event, applies event-specific subscription filters, renders allowlisted Rich
HTML plus a classic HTML fallback, creates
notification_jobs(client-generated UUID, optionalmore_jsonfor pull requests), and returns200. - The outbox worker locks pending jobs with
FOR UPDATE SKIP LOCKED, sends Telegram messages, falls back through media-free Rich HTML, classic HTML, and plain text when Telegram rejects content, marks successes assent, retries temporary failures, and marks permanent failures asfailed.
- Long polling keeps local development simple and avoids exposing Telegram webhook routes.
- One service keeps the codebase small. The durable outbox lives in PostgreSQL instead of adding separate queue infrastructure.
- Branchy owns the
branchyschema and, in production, connects to the sharedcore-postgresas a dedicated least-privilege role (branchy_core) withsearch_path=branchyon a single pool. Domain tables reference shared identity in thecoreschema by natural key (Telegram id) and callcore.touchschema-qualified on that same pool — there is no separatecoreconnection. The old standalonebranchy-postgrescontainer is retired (its volume kept for rollback). - GitHub webhooks are owned per repository, not per subscription. Active subscription events are unioned into the repository hook configuration. When the active event union becomes empty, Branchy deletes its matching hook. The union and in-process lock are keyed by the stable GitHub repository id, so repository renames do not split synchronization state. If a subscription mutation cannot synchronize GitHub, Branchy compensates the database change and best-effort restores the previous hook configuration before returning the error to Telegram.
- GitHub webhook requests validate signature, dedupe delivery IDs, create
durable notification jobs, and return
200without waiting for Telegram. - The notification worker retries Telegram
429and temporary failures usingretry_atandattempts(jittered backoff); permanent Telegram API errors are marked failed. Content, permission, size, and method-availability errors step through media-free Rich HTML, persisted classic HTML, and plain text. Each stage has a fresh timeout, while ambiguous transport failures never fall through and risk duplicate delivery. - When a destination becomes permanently unreachable (bot blocked, kicked, or
chat deleted), the worker auto-pauses the owning subscription
(
pause_reason = 'telegram_blocked') so it stops queuing jobs that can only fail. - A confirmed-invalid GitHub token (HTTP 401) is surfaced to the user as a reconnect prompt; no state is mutated, since a revoked authorization already stops GitHub deliveries.
/healthzand/metricsreport operational health and Prometheus counters with counts only (no secrets).- Telegram API errors are reported by method name, not full bot URL, so the bot
token is not copied into logs or UI errors.
retry_afterrate-limit responses are retried once. - SQL migrations run at startup by default and are recorded in
schema_migrations. - Telegram update offset is persisted in
runtime_state. - Notification payloads are versioned in the outbox. Existing
rich_markdown_v1rows fromv1.1.0-alpha.1/alpha.2are converted through the current Rich HTML sanitizer at delivery time; newrich_html_v1rows store Rich HTML separately while the legacytextcolumn remains a rollback-safe classic HTML payload. - Group delivery is configured only in DM. Candidate groups are learned from
my_chat_memberupdates when the bot is added to a group. - The group-scoped
/startcommand is ephemeral under Bot API 10.2. Its DM prompt targets only the invoking user via Bot API 10.3ephemeral_message_parametersand Branchy does not emit a public group fallback. Command registration retries independently after transient failures, and the reply precedes the best-effortcore.touchpresence write. - Before saving a group destination, Branchy checks
getChatMemberand requirescreatororadministrator. - Telegram callback data uses short tokens stored in PostgreSQL for dynamic
settings actions. Static menu actions use short literal callback strings.
Pull-request More uses a static
m:prefix plus the compact notification job UUID and does not go throughcallback_tokens(those rows are bound tocore.personand the tapping user). The handler looks upnotification_jobsby id anddestination_chat_id, then sends an extra ephemeral Rich Message to the tapping user. The public card is not replaced. Job ids are generated in Go so the More button can embed them before INSERT;more_jsonholds the PR webhook header snapshot (NULL for push/release and older jobs). Tapping More loads the file list with the subscription owner's GitHub token and does not persist patches. The webhook handler never calls the files API.