Bridge service that forwards software-forge webhooks (Gitea, Forgejo) to Apprise for delivery through 100+ notification services — Telegram, Discord, Slack, email, and more.
  • Python 98.6%
  • Shell 0.8%
  • Dockerfile 0.6%
Find a file
Eric Schewe db5b6cde13
All checks were successful
Build and Publish Docker Image / test (push) Successful in 26s
Build and Publish Docker Image / build-and-push (push) Successful in 17s
Merge add-commit-status-event: v1.5.1 Gitea commit status event
2026-06-09 22:17:39 -07:00
.forgejo/workflows ci: revert Docker Hub namespace to DOCKERHUB_USER secret 2026-06-09 19:29:44 -07:00
app v1.5.1: add Gitea commit status event formatter 2026-06-09 20:41:53 -07:00
tests v1.5.1: add Gitea commit status event formatter 2026-06-09 20:41:53 -07:00
.dockerignore Initial implementation of Gitea-Apprise webhook bridge 2026-02-08 16:33:45 -08:00
.env.example v1.4.0: handle Forgejo-only events + Authorization-header auth 2026-06-09 19:26:46 -07:00
.gitignore Updated gitignore 2026-03-13 19:41:49 -07:00
Claude.md v1.5.1: add Gitea commit status event formatter 2026-06-09 20:41:53 -07:00
docker-compose.yml v1.4.0: handle Forgejo-only events + Authorization-header auth 2026-06-09 19:26:46 -07:00
docker-entrypoint.sh Reliability, observability, and CI improvements (v1.1.2) 2026-03-14 00:06:25 -07:00
Dockerfile v1.2.1: bump Python 3.12 → 3.14 2026-05-17 16:17:57 -07:00
LICENSE Added GPL3 2026-02-08 23:06:47 -08:00
pyproject.toml v1.5.1: add Gitea commit status event formatter 2026-06-09 20:41:53 -07:00
README.md v1.5.1: add Gitea commit status event formatter 2026-06-09 20:41:53 -07:00
requirements-dev.txt Migrate to async: Quart + httpx + uvicorn (v1.1.0) 2026-03-13 23:06:19 -07:00
requirements.txt Migrate to async: Quart + httpx + uvicorn (v1.1.0) 2026-03-13 23:06:19 -07:00
SECURITY.md v1.3.0: native Gitea + Forgejo webhook support 2026-06-09 19:14:26 -07:00

Forge-Apprise Webhook Bridge

A lightweight bridge service that forwards Gitea and Forgejo webhook events to Apprise notifications. Get notified about pushes, issues, pull requests, releases, and more through any of Apprise's 100+ supported notification services (Telegram, Discord, Slack, email, etc.).

Gitea and Forgejo are webhook-compatible today, and this bridge supports both natively: it accepts either X-Gitea-* or X-Forgejo-* headers, so it keeps working even if the two projects' header conventions diverge in the future.

AI Disclosure

This container was completely written by Claude AI.

How It Works

Gitea/Forgejo  --(webhook)-->  Bridge  --(API call)-->  Apprise API  -->  Your notification services
  1. You configure a webhook in Gitea or Forgejo pointing at the bridge
  2. When events happen in your repo, the forge sends the event data to the bridge
  3. The bridge formats it into a human-readable message and forwards it to your Apprise API instance
  4. Apprise delivers the notification to your configured services

Features

  • Native Gitea and Forgejo support: accepts X-Gitea-* or X-Forgejo-* webhook headers, verifying the HMAC signature against whichever the forge sends
  • 25+ event types supported: push, issues, pull requests, comments, reviews, review requests, releases, wiki, branch/tag create/delete, forks, packages, CI runs (Forgejo Actions + Gitea workflows), and more
  • Markdown and plain-text formatting with clickable links, bold text, and smart truncation
  • HMAC-SHA256 signature verification to ensure webhooks are actually from your forge
  • Two Apprise modes: stateful (pre-configured key) or stateless (direct notification URLs)
  • Event filtering to only forward the events you care about
  • Apprise tags for routing notifications to specific services
  • Stateless and lightweight -- no database, no storage, just a single container
  • Health check endpoint at /health

Quick Start

1. Run the bridge

# docker-compose.yml
services:
  apprise-forge-bridge:
    image: thefizi/apprise-forge-notifications
    ports:
      - "5000:5000"
    environment:
      - WEBHOOK_SECRET=your-shared-secret
      - APPRISE_BASE_URL=http://your-apprise-instance:8000
      - APPRISE_STATEFUL_KEY=forge
    restart: unless-stopped
docker-compose up -d

2. Configure the webhook

In Gitea or Forgejo:

  1. Go to your repository (or organization) Settings > Webhooks > Add Webhook
  2. For the webhook type, select Gitea (on Gitea) or Forgejo (on Forgejo)
  3. Set Target URL to http://your-bridge-host:5000/webhook
  4. Set Secret to the same value as WEBHOOK_SECRET
  5. Choose which events to trigger on (or select "All Events")
  6. Save

Note: Depending on your server configuration you may need to allow the bridge's host in the webhook allow-list (Gitea / Forgejo: the same [webhook].ALLOWED_HOST_LIST setting).

3. Test it

Push a commit or open an issue -- you should receive a notification through Apprise.

Configuration

All configuration is done through environment variables.

Variable Required Default Description
APPRISE_BASE_URL Yes URL of your Apprise API instance (e.g. http://apprise:8000)
APPRISE_STATEFUL_KEY One of these Apprise configuration key for stateful mode (uses /notify/{key})
APPRISE_STATELESS_URLS One of these Apprise notification URLs for stateless mode (e.g. tgram://bot/chat,slack://token)
WEBHOOK_SECRET Recommended (empty) Shared secret for HMAC-SHA256 signature verification. The legacy name GITEA_WEBHOOK_SECRET is still accepted as a deprecated alias.
WEBHOOK_AUTH_HEADER No (empty) Expected Authorization header value (e.g. Bearer s3cr3t). When set, requests must present a matching header. Use instead of, or together with, WEBHOOK_SECRET — if both are set, both must pass.
APPRISE_TAGS No (empty) Comma-separated Apprise tags included on every notification
NOTIFICATION_FORMAT No markdown Message format: markdown or text
ALLOWED_EVENTS No (empty = all) Comma-separated list of event types to forward
LOG_LEVEL No INFO Logging level: DEBUG, INFO, WARNING, ERROR
APPRISE_TIMEOUT No 30 Read timeout in seconds for Apprise API calls
APPRISE_CONNECT_TIMEOUT No 5 Connection timeout in seconds when reaching the Apprise API. A shorter value than APPRISE_TIMEOUT means a down Apprise instance fails fast rather than holding the forge's connection open.
LISTEN_HOST No 0.0.0.0 Host address to bind the server to. When BEHIND_PROXY=true and the proxy is on the same host, consider setting this to 127.0.0.1 to prevent direct external access to the bridge port. (entrypoint-only — not read by the app)
LISTEN_PORT No 5000 Port to bind the server to. (entrypoint-only — not read by the app)
BEHIND_PROXY No false Set to exactly true to pass --proxy-headers to uvicorn when behind a reverse proxy (Nginx, Traefik, etc.). (entrypoint-only — not read by the app)
PROXY_TRUSTED_IPS No 127.0.0.1 IP(s) trusted to set X-Forwarded-For headers. Only used when BEHIND_PROXY=true. Accepts a single IP, a CIDR range, or a comma-separated list. Do not set to * unless your proxy is on a fully private network. (entrypoint-only — not read by the app)
UVICORN_NO_ACCESS_LOG No false Set to true to suppress uvicorn's per-request access logs. Useful when a reverse proxy already handles access logging, or to reduce log noise from health check polling. (entrypoint-only — not read by the app)

Note: Do not wrap env var values in quotes in your .env file — the bridge strips them automatically. For example, use WEBHOOK_SECRET=mysecret, not WEBHOOK_SECRET="mysecret".

Apprise Modes

You must configure one of the two Apprise modes:

Stateful mode (APPRISE_STATEFUL_KEY): Your Apprise API instance has pre-configured notification URLs saved under a key. The bridge sends to /notify/{key}. This is the simplest setup if you already manage your Apprise configuration.

Stateless mode (APPRISE_STATELESS_URLS): You provide the Apprise notification URLs directly. The bridge sends them with each request to /notify/. Useful if you don't want to configure state in Apprise.

If both are set, stateful mode takes precedence.

Event Filtering

By default, the bridge forwards all events it receives. To limit which events are forwarded, set ALLOWED_EVENTS to a comma-separated list:

ALLOWED_EVENTS=push,issues,pull_request,release

Events not in the list are silently skipped (HTTP 200 returned to the forge so it doesn't retry).

Supported Events

Event Example Notification Severity
Push [user/repo] 3 new commits to main info
Issue opened [user/repo] Issue #42 opened info
Issue closed [user/repo] Issue #42 closed success
Issue reopened [user/repo] Issue #42 reopened warning
Issue assigned/labeled/milestoned [user/repo] Issue #42 assigned info
PR opened [user/repo] PR #15 opened info
PR merged [user/repo] PR #15 merged success
PR closed (not merged) [user/repo] PR #15 closed warning
PR assigned/labeled/milestoned/synced [user/repo] PR #15 synchronized info
PR review approved [user/repo] PR #15 approved success
PR review changes requested [user/repo] PR #15 changes requested failure
Comment (issue or PR) [user/repo] Comment on issue #42 info
Review comment [user/repo] Review comment on PR #15 info
Release published [user/repo] Release v1.0.0 published success
Branch/tag created [user/repo] Branch created: feature-x info
Branch/tag deleted [user/repo] Branch deleted: feature-x warning
Fork [user/repo] Repository forked info
Wiki [user/repo] Wiki page edited: Home info
PR review requested [user/repo] PR #15 review requested info
Package published [org/app] Package created: img 1.0 success
Package deleted [org/app] Package deleted: img 1.0 warning
Action run (Forgejo CI) [user/repo] Action run failed: ci.yml failure/success
Workflow run (Gitea CI) [user/repo] Workflow run success: Build success/failure/warning
Workflow job (Gitea CI) [user/repo] Workflow job failure: build success/failure/warning
Commit status (Gitea) [user/repo] Commit status success: ci/build success/failure/warning

The severity maps to Apprise notification types (info, success, warning, failure), which some notification services render differently (e.g. different colors or icons).

CI runs are covered on both forges: Forgejo emits action_run_failure/success/recover, while Gitea emits workflow_run/workflow_job plus commit status — the bridge handles all of them (Gitea's run/job/status are reported only on a terminal result, not on queued/in-progress/pending). Any event without a dedicated formatter is accepted and silently skipped with HTTP 200, so unhandled event types never cause webhook retries.

Security

  • Webhook signature verification: Set WEBHOOK_SECRET to a strong shared secret. The bridge verifies every incoming request using HMAC-SHA256 — against X-Gitea-Signature or X-Forgejo-Signature, whichever the forge sends — before processing it. Without this, anyone who knows your bridge URL could send fake notifications.
  • Non-root container: The Docker image runs as a non-root user.
  • No secrets in logs: Webhook secrets and Apprise URLs are never logged.

Development

Running locally

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
APPRISE_BASE_URL=http://localhost:8000 APPRISE_STATEFUL_KEY=test uvicorn app:create_app --factory --reload

Running tests

source .venv/bin/activate
pytest -v                          # run all tests
pytest --cov=app --cov-report=term-missing  # with coverage report

Project structure

app/
  __init__.py          # Quart app factory
  config.py            # Environment variable configuration
  webhook.py           # POST /webhook endpoint (multi-forge header handling)
  signature.py         # HMAC-SHA256 verification
  apprise_client.py    # Apprise API HTTP client
  formatters/          # One module per event type
    base.py            # Shared formatting helpers
    push.py, issues.py, pull_request.py, comments.py,
    review.py, repository.py, release.py, wiki.py

License

GNU General Public License v3.0