A lightweight webhook router that receives incoming webhooks on unique endpoints and fans them out to multiple destinations in parallel. Designed for self-hosted Git platforms (Gitea, GitLab) with signature verification support for those platforms and GitHub. It forwards raw requests (headers + body) so it may work with other webhook sources too, but that's not tested or officially supported.
π Changelog
- π Single ingress point β expose one webhook URL instead of many
- π‘ Fan out to multiple targets β CI, chat notifications, monitoring, etc. all from one hook
- π Configure routing outside your Git host β no need to add N webhooks per repo
- π Keep internal services private β only the fanout server needs to be reachable
# Clone and start
git clone https://github.com/Xalior/GitTaskFanOut.git && cd GitTaskFanOut
docker compose up -d
# Open http://localhost:6175 β register an admin account on first run# Prerequisites: Node.js 22+, pnpm
pnpm install
pnpm dev # development (http://localhost:6175)
pnpm build && pnpm start # productionAll config lives in the data/ directory as INI files:
| File | Purpose |
|---|---|
routes.ini |
Webhook routes (managed via UI) |
users.ini |
Admin user accounts (created via UI) |
auth.ini |
Optional pepper for password hashing |
theme.css |
Optional theme CSS (replaces Bootstrap entirely) |
| Variable | Default | Description |
|---|---|---|
SESSION_SECRET |
dev default | Session encryption key (min 32 chars) |
AUTH_PEPPER |
(none) | Appended to passwords before bcrypt hashing |
PORT |
6175 |
Server listen port |
NODE_ENV |
development |
Set to production for prod |
BIND_ADDRESS |
0.0.0.0 |
Docker host bind IP (e.g. 172.17.0.1 to restrict to Docker bridge) |
SMTP_URL |
(none) | SMTP connection URL for password reset emails |
SMTP_FROM |
noreply@localhost |
From address for password reset emails |
Set via AUTH_PEPPER env var (recommended) or in data/auth.ini:
[_meta]
pepper = your-random-string-here- π Create a route in the UI β give it a name, add target URLs
- π Copy the webhook URL β e.g.
/api/hooks/a1b2c3d4e5f6g7h8 - π Configure your Git host to send webhooks to that URL
- π Incoming webhooks are forwarded to all targets in parallel
- π Failed targets are retried up to 3 times with backoff
If a secret is configured on a route, incoming webhooks are verified:
- GitHub β
X-Hub-Signature-256(HMAC-SHA256) - Gitea β
X-Gitea-Signature(HMAC-SHA256) - GitLab β
X-Gitlab-Token(plain token comparison)
curl http://localhost:6175/api/health
# β {"status":"ok","version":"0.1.0","uptime":1234,"startedAt":"..."}Used by the Docker Compose health check and compatible with any load balancer or monitoring system.
The app ships with stock Bootstrap and supports full theme replacement at runtime. Themes include Bootstrap itself β they're not layered on top β so every aspect of the UI can be customised.
The endpoint /api/theme.css serves the active theme CSS:
- Custom theme present (
data/theme.css) β serves your theme - No custom theme β falls back to stock
bootstrap.min.css
The app loads this single CSS endpoint and has no build-time Bootstrap dependency. Dark/light mode is supported via Bootstrap 5's data-bs-theme attribute, with a theme toggle in the navbar that persists the preference in a cookie.
Drop a compiled CSS file into data/theme.css and reload. Remove the file to revert to stock Bootstrap.
cp themes/nbn24/dist/nbn24.css data/theme.css # activate
rm data/theme.css # revertThe themes/nbn24/ directory contains an example theme based on the Bootswatch "Pulse" colour scheme β purple primary, no rounded corners, custom component styling. See themes/nbn24/README.md for build instructions.
A theme is a standalone project that compiles a full Bootstrap 5 CSS bundle. Use themes/nbn24/ as a starting point:
cp -r themes/nbn24 themes/mytheme
cd themes/mytheme- Edit
scss/_variables.scssβ override any Bootstrap SCSS variable - Edit
scss/_bootswatch.scssβ add component-level style overrides (applied after Bootstrap) - Rename the entry point if you like (
scss/mytheme.scss) and updatepackage.jsonscripts accordingly - Build and deploy:
npm install && npm run build
cp dist/mytheme.css ../../data/theme.cssThemes are out-of-tree β they have their own package.json, node_modules, and build step. They don't depend on the webapp's build system and don't require Docker. Any tool that produces a CSS file (Sass, PostCSS, plain CSS, a Bootswatch download) will work, as long as it includes Bootstrap.
- π Passwords hashed with bcrypt (12 rounds) + optional pepper
- πͺ HTTP-only session cookies (iron-session)
- π« IP-based rate limiting on login/register (10 attempts / 15 min)
- β Webhook signature verification (GitHub, Gitea, GitLab)
- π All route management APIs require authentication
- π‘ Webhook ingress endpoints are unauthenticated (as expected by Git platforms)
GitTaskFanOut/
βββ docker-compose.yml
βββ data/ # Config directory (gitignored)
β βββ routes.ini
β βββ users.ini
β βββ auth.ini
β βββ theme.css # Optional: custom theme (replaces Bootstrap)
βββ themes/ # Out-of-tree theme projects
β βββ nbn24/ # Example theme (Bootswatch Pulse)
β βββ package.json
β βββ scss/
βββ webapp/ # Next.js app
βββ Dockerfile
βββ server.ts # Custom server with morgan logging
βββ src/
βββ components/ # ThemeDropdown (dark/light/auto toggle)
βββ lib/ # Config, auth, relay, session, rate limiting
βββ styles/ # App-specific CSS (no Bootstrap β served at runtime)
βββ pages/
βββ _document.tsx # FOUC-free theme initialisation
βββ index.tsx # React-Bootstrap UI
βββ api/
βββ health.ts
βββ theme.css.ts # Serves custom theme or stock Bootstrap
βββ auth/ # login, logout, register, change-password, status
βββ hooks/ # [...slug].ts β webhook receiver
βββ routes/ # CRUD + ping + duplicate
LGPL-3.0 β see LICENSE