GitHub / Gitea webhook → Discord / Telegram dispatcher. Receives webhook events via Cloudflare Workers, applies filters, and routes formatted messages to Discord channels/threads and Telegram chats/topics. Forge-specific adapters live under server/lib/providers/ (GitHub + Gitea today; GitLab etc. can be added later).
- 28 event formatters — push, pull_request, issues, issue_comment, workflow_run, workflow_job, status, deployment, deployment_status, check_run, check_suite, ping, release, create, delete, star, fork, pull_request_review, pull_request_review_comment, commit_comment, member, label, milestone, discussion, discussion_comment, repository, code_scanning_alert, dependabot_alert (+ generic fallback, +
customwebhooks) - Multi-provider webhooks — GitHub (
X-Hub-Signature-256) and Gitea (X-Gitea-Signature) share one/webhookendpoint; the provider is auto-detected from headers - Per-group webhook ingress — every group can get its own
POST /webhook/{groupId}URL + secret (Gitea, classic GitHub webhooks, and arbitrary custom JSON posts signed withX-WebHooker-Signature, with optional timestamp+nonce replay protection) - GitHub App tenant isolation — bind a group to a GitHub App installation id so only that org/user's events enter it
- HMAC-SHA256 signature verification (Web Crypto API)
- Filter by event type, repo, actor, action, branch, keyword (supports
*/?globs and/regex/) - Rich messages with color coding, author avatars, fields, and timestamps — rendered as Discord embeds and Telegram HTML
- Route to Discord channels/threads and Telegram chats/topics (multi-target routes)
workflow_run/check_runprogress is edited in place (single message updated as the run advances) on both platforms- Per-group webhook log channel — point a group at a Discord channel/thread or Telegram chat/topic and every webhook the group's routes dispatch is summarized there (✅/❌ per route × target)
- GitHub OAuth for user actions (comment, edit comment, delete comment, merge, close, react)
- Web UI config console (
/admin) — manage routes and groups with GitHub OAuth + admin whitelist, view send logs - Discord Interactions Endpoint (Ed25519-verified) for
/ghslash commands, message context-menu commands, PR merge/close buttons, and comment modals - Telegram
/ghcommands (login/logout/comment/merge/close) via the Telegram webhook, with avatar link-preview cards - Cloudflare D1 for config (routes/groups), send logs, platform account links, dedup, delivery state and message tracking + KV for ephemeral state/cache/security tokens, + optional R2 for oversized payloads
- Async delivery via Cloudflare Queues — when the
QUEUEbinding is present, verified webhooks are enqueued towebhooker-deliveryand dispatched by a consumer with exponential retry backoff (5s/30s/2m/10m) and a dead-letter queue (webhooker-delivery-dlq); oversized payloads are parked in R2 (PAYLOADbinding, falling back to KVqueue:payload:*). Without the binding, dispatch stays inline - Graceful degradation (webhook-only mode if Discord unavailable)
GitHub Webhook → Cloudflare Worker (Nuxt 4 / Nitro)
├── POST /webhook → verify → dedup → enqueue (Queue) → dispatch → Discord (REST) / Telegram (Bot API)
├── POST /discord/interactions → verify (Ed25519) → handle command/button/modal
├── POST /telegram/webhook → verify (secret token) → handle /gh commands
├── GET /auth/github → OAuth flow
├── GET /api/richheader → Telegram avatar link-preview card
├── POST /api/* → user actions (Bearer token auth)
├── /admin → routes, groups & send logs Web UI
└── GET /health → status check
- Cloudflare Worker — HTTP ingress, signature verification, routing, platform dispatch
- Interactions Endpoint — HTTPS callback (no Discord Gateway connection, no Durable Object); the bot stays offline and commands are registered via the API
- KV — cache + ephemeral state: token storage (
token:{userId}), OAuth state (state:{hex}), admin sessions (session:{id}), per-group webhook secrets (tenant:{groupId}), invites, config cache, delivery dedup/state/message-tracking fallback (delivery:*,delivery-state:*,msg:*used only when D1 is unavailable) and message-update locks (msg:lock:*) - D1 — source of truth for routes/groups (
d1_routes/d1_groups), send logs (send_logs), audit logs (audit_logs), dedup (dedup_keys), delivery state (delivery_state), message-update tracking (message_tracking), Discord↔GitHub links (discord_links), Telegram↔GitHub links (telegram_links) - Queue — async delivery when
QUEUEis bound:webhooker-delivery(exponential retry) + DLQwebhooker-delivery-dlq; oversized payloads parked in R2 (PAYLOADbinding,webhooks/YYYY/MM/DD/*.json, falling back to KVqueue:payload:*)
bun install # package manager is bun (lockfile: bun.lock)
cp .env.example .dev.vars # Fill in secrets for local dev
bun run build # Production build first (wrangler dev serves the built worker)
bunx wrangler dev # Start local dev server| Variable | Description |
|---|---|
GITHUB_WEBHOOK_SECRET |
Webhook secret from GitHub |
GITEA_WEBHOOK_SECRET |
Webhook secret from Gitea (required only to receive Gitea webhooks) |
GITHUB_APP_ID |
GitHub App ID (used by the App install flow to resolve the installing account) |
GITHUB_PRIVATE_KEY |
App private key (PKCS#8 PEM; used by the App install flow; optional) |
GITHUB_CLIENT_ID |
OAuth client ID |
GITHUB_CLIENT_SECRET |
OAuth client secret |
DISCORD_TOKEN |
Bot token |
DISCORD_PUBLIC_KEY |
Discord application public key (from the Developer Portal) — required for interactions |
DISCORD_APPLICATION_ID |
Discord application id (optional; auto-resolved via GET /oauth2/applications/@me if omitted) |
TELEGRAM_TOKEN |
Telegram bot token (from BotFather) — required for Telegram routes |
TELEGRAM_WEBHOOK_SECRET |
Optional secret token for POST /telegram/webhook verification |
TELEGRAM_RICH_HEADER_HOST |
Optional base URL overriding the built-in GET /api/richheader for Telegram avatar cards |
BASE_URL |
Public URL for OAuth callbacks and the Telegram webhook sync |
ADMIN_USER_IDS |
Comma-separated GitHub user IDs (or logins) allowed to access /admin |
ALLOW_SELF_SIGNUP |
1 to give access-less GitHub users a personal group on first login (default off) |
AUDIT_RETENTION_DAYS |
Audit-log retention in days for the scheduled cleanup (default 90) |
NUXT_PUBLIC_DOCS_URL |
Optional docs site URL used by the landing page |
NUXT_PUBLIC_REPO_URL |
Optional GitHub repo URL used by the landing page |
NUXT_PUBLIC_LEGAL_CONTACT |
Optional contact shown on /terms and /privacy |
Routes are stored in D1 (d1_routes, seeded from legacy KV config:routes on first load). There are no default routes — every route (including its target) must be defined explicitly, either via the Web UI (/admin) or by storing a JSON array in D1. A route may carry multiple targets, so one rule can forward to several channels at once:
[
{
"id": "all-push",
"name": "Push Events",
"enabled": true,
"groupId": "default",
"filters": [{ "type": "event", "match": "push" }],
"stop": true,
"targets": [
{ "platform": "discord", "channelId": "CHANNEL_ID" },
{ "platform": "telegram", "chatId": "-1001234567890" }
]
}
]target.platform selects the push target: discord (default) or telegram. Discord targets require target.channelId (optional threadId for a thread); Telegram targets require target.chatId (optional topicId for a topic). Routes belong to groups (D1 d1_groups, seeded from legacy KV config:groups) that scope admin access and can restrict which org/user events flow in. See the Routes & Targets and Groups & Access Control guides for the full schema.
The built-in config console lets you manage routes and groups in the browser (add / edit / delete / toggle / reorder), inspect send logs, manage group members and invite links, and read the audit log — no KV access needed:
- Set
ADMIN_USER_IDSto the GitHub user IDs (or logins) allowed to manage the console, e.g.ADMIN_USER_IDS=12345,RhenCloud. - Visit
/adminand sign in with GitHub. Users with no access get403— unlessALLOW_SELF_SIGNUP=1(they receive a personal group) or they follow a group invite link. - Changes are written to D1 immediately, the config cache is invalidated, and the webhook pipeline picks them up on the next run.
Sign out at /admin/logout. Every group has members with a role (owner / admin / viewer); all admin operations are recorded in the D1 audit_logs table.
Every filter supports plain text, */? globs, and /regex/ patterns (case-insensitive); set exclude: true to invert. Filters can also be grouped into an AST via an optional ast on a route (all / any / not nodes) to express boolean combinations beyond the default AND-list. See the Filter Tutorial for the pattern syntax and the full filter reference.
GET /health— Returns{"status": "ok"}
GET /auth/github— Start GitHub OAuth flow (redirects to GitHub)GET /auth/github/callback— OAuth callback (exchanges code for token; admin session / Discord link / Telegram link)DELETE /auth/token/:userId— Revoke user token
POST /api/comment— Create issue commentPOST /api/merge— Merge pull requestPOST /api/close— Close pull requestPOST /api/react— Add reaction to issue
GET /admin— Config console UIGET /admin/login— Start admin sign-in (GitHub OAuth)GET /admin/logout— Sign outGET /admin/invite?token=…— Accept a group invite (browser page)GET /admin/api/routes— List routesPUT /admin/api/routes— Replace routes (owner/admin per group)GET /admin/api/groups— List groups + your role in eachPUT /admin/api/groups— Replace groups (super: all; owner: own groups only)GET /admin/api/groups/:groupId/routes— List a group's routesPUT /admin/api/groups/:groupId/routes— Replace a group's routesPOST /admin/api/groups/:groupId/invites— Create invite link (owner)GET /admin/api/groups/:groupId/invites— List pending invites (owner)DELETE /admin/api/invites/:token— Revoke an invite (owner)GET /admin/api/audit— Audit log (scoped)GET /admin/api/me— Current session / scope / rolesGET /admin/api/logs— Send logs (scoped)GET /admin/api/logs/:id— Single send-log entryGET /admin/api/metrics— Delivery stats (totals, failure rate, per-platform/event/status, recent failures); optional?groupId=scopeGET /admin/api/delivery/:deliveryId— All send-log attempts for one delivery
- GitHub App — create the app, subscribe to events, configure OAuth, and set the Setup URL for tenant isolation: see GitHub App Setup
- Discord bot — create the bot, invite it with
applications.commands(combined permission integer274877910016), and configure the Interactions Endpoint: see Discord Bot Setup. The bot never connects to the Discord Gateway, so it shows as offline — messaging is unaffected (always REST). - Telegram bot — create the bot with @BotFather, set
TELEGRAM_TOKEN(optionalTELEGRAM_WEBHOOK_SECRET); the webhook is synced automatically by the scheduled trigger: see Telegram Bot Setup - Deployment — KV namespace, D1 database + migrations, optional Queues, secrets, and deploy: see the Deployment guide
The bot registers native slash and message context-menu commands, synced by the scheduled trigger (every 5 minutes): per-guild for instant availability, and globally (24h dedup, ~1h propagation). After /gh login you can comment on issues/PRs as yourself, edit/delete your comments, and merge/close PRs via buttons — all replies are ephemeral and GitHub enforces permission.
/gh login /gh logout
/gh comment add|edit|del link:<url> (or right-click a notification → Apps → GitHub: 添加/编辑/删除评论)
See the full reference in the Bot Commands guide.
bun run dev # Nuxt dev server (HMR)
bun run typecheck # Type checking
bun run lint # ESLint
bun test # Unit tests28 event formatters (push, pull_request, issues, workflow_run, release, ...) plus custom webhooks; unsupported events fall back to a generic formatter. See the full table with embed highlights in Supported Events.
MIT