A self-hosted service that forwards GitHub webhook events to Discord channels with beautifully formatted embeds. Supports multi-user, multi-repo configurations with secure webhook signature verification.
- On first start, the deployment will automatically create an invite code for you to use. You can use this to sign up for an account.
- Secure by design - GitHub webhook signature verification (HMAC-SHA256)
- Multi-user support - Each user manages their own webhook mappings
- Flexible registration - Open, closed, or invite-only modes
- Beautiful Discord embeds - Rich formatting for PR events
- Easy deployment - Single binary, auto-migrations, Railway-ready
- Bun runtime
- PostgreSQL database
# Clone the repository
git clone https://github.com/alexng353/github-discord-webhook.git
cd github-discord-webhook
# Install dependencies
bun install
# Set up environment variables (see below)
cp .env.example .env
# Run the server (auto-runs migrations)
bun run start| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ | - | PostgreSQL connection string (postgres://...) |
PORT |
❌ | 3000 |
Server port |
NODE_ENV |
❌ | development |
Environment mode |
REGISTRATION |
❌ | open |
Registration mode: open, closed, or invite_only |
RAILWAY_PUBLIC_DOMAIN |
❌ | - | Public domain for generated webhook URLs (Railway deployment) |
DATABASE_URL=postgres://user:password@localhost:5432/webhooks
PORT=3000
REGISTRATION=invite_onlyGitHub Repo -> GitHub Webhook -> This Service -> Discord Webhook -> Discord Channel
- User registers and creates a webhook mapping (repo → Discord webhook)
- Service generates a unique GitHub webhook URL
- User configures this URL in their GitHub repository settings
- GitHub sends webhook events to the service
- Service verifies the signature and forwards formatted embeds to Discord
The service has two authentication systems:
Used for managing webhook mappings through the web interface or API.
POST /auth/register → Create account
POST /auth/login → Get session cookie (7-day TTL)
POST /auth/logout → Invalidate session
GET /auth/me → Get current user info
Each webhook mapping has a secret that you also configure in GitHub. When GitHub sends a webhook:
- GitHub signs the payload with your secret using HMAC-SHA256
- GitHub sends the signature in
X-Hub-Signature-256header - This service verifies the signature before processing
This ensures only legitimate GitHub webhooks are processed.
Configure via REGISTRATION environment variable:
| Mode | Description |
|---|---|
open |
Anyone can register |
closed |
No new registrations allowed |
invite_only |
Requires an invite code from existing user |
When running in invite_only mode, the first invite code is printed to the console on startup.
# Check registration mode
curl http://localhost:3000/auth/registration-mode
# Register (if open or with invite code)
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"username": "myuser", "password": "mypassword123", "inviteCode": "abc123"}'
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "myuser", "password": "mypassword123"}' \
-c cookies.txt- Open Discord and go to your server
- Right-click the channel → Edit Channel → Integrations → Webhooks
- Click New Webhook, customize name/avatar
- Click Copy Webhook URL
curl -X POST http://localhost:3000/webhooks/mapping \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"repo": "owner/repo-name",
"webhookUrl": "https://discord.com/api/webhooks/123/abc...",
"secret": "your-github-webhook-secret"
}'Response:
{
"created": true,
"repo": "owner/repo-name",
"id": "uuid-here",
"githubWebhookUrl": "https://your-domain.com/webhook/github/uuid-here"
}- Go to your GitHub repository → Settings → Webhooks → Add webhook
- Payload URL: Use the
githubWebhookUrlfrom the previous step - Content type:
application/json - Secret: The same secret you used when creating the mapping
- Events: Select "Pull requests" (or "Let me select individual events")
- Click Add webhook
Create or close a pull request in your repository. You should see a beautifully formatted embed in your Discord channel!
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/auth/registration-mode |
- | Get current registration mode |
POST |
/auth/register |
- | Create new account |
POST |
/auth/login |
- | Login, returns session cookie |
POST |
/auth/logout |
Session | Logout, clears session |
GET |
/auth/me |
Session | Get current user info |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/auth/invites |
Session | Create new invite code |
GET |
/auth/invites |
Session | List your invite codes |
DELETE |
/auth/invites/:code |
Session | Revoke unused invite code |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/webhooks/mapping |
Session | Create new repo→Discord mapping |
GET |
/webhooks/mapping |
Session | List your webhook mappings |
GET |
/webhooks/mapping/:repo |
Session | Get specific mapping info |
DELETE |
/webhooks/mapping/:repo |
Session | Delete a mapping |
PATCH |
/webhooks/mapping/:repo/secret |
Session | Update webhook secret |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/webhook/github/:id |
GitHub Signature | Receive GitHub webhook events |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check endpoint |
Currently supported:
- Pull Request Opened - Green embed with PR details
- Pull Request Closed - Red embed (or purple if merged)
More events coming soon!
# Run in development mode with hot reload
bun run dev
# Lint and format
bun run lintThe service uses PostgreSQL with Drizzle ORM. Migrations run automatically on startup.
users- User accounts (id, username, password_hash)sessions- Login sessions with expiryauth_tokens- Bearer tokens for API authwebhook_mappings- Repo → Discord webhook mappingsinvite_codes- Registration invite codes
- Connect your GitHub repo to Railway
- Add a PostgreSQL database
- Set environment variables:
DATABASE_URL(auto-set by Railway if using their Postgres)REGISTRATION=invite_only
- Deploy!
The RAILWAY_PUBLIC_DOMAIN is automatically detected for generating webhook URLs.
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "start"]bun install --production
NODE_ENV=production bun run start- Passwords are hashed with Argon2 (via Bun's built-in
Bun.password) - GitHub webhook signatures use timing-safe comparison
- Sessions expire after 7 days
- Discord webhook URLs are redacted in API responses
- Each webhook mapping is scoped to its owner
Github-Discord-Webhook-Bridge Copyright (C) 2025 Alexander Ng
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.