216 lines
9.5 KiB
Markdown
216 lines
9.5 KiB
Markdown
# Notify Bridge
|
|
|
|
A generic bridge between service providers and notification targets.
|
|
|
|
Notify Bridge monitors services (Immich, Gitea, Planka, NUT, Google Photos, generic webhooks,
|
|
and internal scheduler) for changes and dispatches notifications to configurable targets
|
|
(Telegram, Discord, Slack, Matrix, ntfy, email, generic webhooks) using customizable templates.
|
|
|
|
## Architecture
|
|
|
|
- **Service Providers** — Connectors to external services (Immich, Gitea, Planka, NUT, Google Photos, generic Webhook, internal Scheduler)
|
|
- **Trackers** — Monitor specific collections within a provider for changes
|
|
- **Tracking Configs** — Define what events to watch for and scheduling rules
|
|
- **Notification Targets** — Where to send notifications (Telegram, Discord, Slack, Matrix, ntfy, email, webhook URLs)
|
|
- **Template Configs** — Jinja2 templates that format notifications per provider type
|
|
|
|
## Project Structure
|
|
|
|
```text
|
|
packages/
|
|
core/ — Shared library: providers, models, notifications, templates
|
|
server/ — FastAPI REST server with SQLite database
|
|
frontend/ — SvelteKit dashboard (Svelte 5, Tailwind CSS v4)
|
|
```
|
|
|
|
## Quick Docker Deploy
|
|
|
|
```bash
|
|
docker run -d \
|
|
--name notify-bridge \
|
|
--restart unless-stopped \
|
|
-p 8420:8420 \
|
|
-v notify-bridge-data:/data \
|
|
-e NOTIFY_BRIDGE_SECRET_KEY=$(openssl rand -hex 32) \
|
|
-e NOTIFY_BRIDGE_CORS_ALLOWED_ORIGINS=http://localhost:8420 \
|
|
git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge:latest
|
|
```
|
|
|
|
Then open `http://localhost:8420` in your browser.
|
|
|
|
### Environment Variables
|
|
|
|
Core settings (all prefixed with `NOTIFY_BRIDGE_`):
|
|
|
|
| Variable | Required | Default | Description |
|
|
| -------- | -------- | ------- | ----------- |
|
|
| `SECRET_KEY` | Yes | — | Secret for JWT signing (min 32 chars). Default placeholders and known dev-only strings are rejected on startup. |
|
|
| `CORS_ALLOWED_ORIGINS` | Recommended | `http://localhost:5175` | Comma-separated browser origins. Wildcard `*` is **rejected** because credentials are enabled. Set this to the URL you load the UI from. |
|
|
| `DATA_DIR` | No | `/data` (in Docker) | Directory for SQLite DB, backups, and caches. Mount a volume here. |
|
|
| `DATABASE_URL` | No | `sqlite+aiosqlite:///<DATA_DIR>/notify_bridge.db` | Override DB connection string. |
|
|
| `HOST` | No | `0.0.0.0` | Bind address. |
|
|
| `PORT` | No | `8420` | Server listen port. |
|
|
| `DEBUG` | No | `false` | Enable debug logging. |
|
|
|
|
Reverse proxy / network:
|
|
|
|
| Variable | Default | Description |
|
|
| -------- | ------- | ----------- |
|
|
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | Trusted proxy IPs whose `X-Forwarded-For` / `X-Forwarded-Proto` headers are honored. Set to your reverse proxy IP (e.g. `172.17.0.1` for the default Docker bridge). Use `*` only when the container is not directly internet-reachable. |
|
|
| `EXTERNAL_URL` | — | Public base URL (e.g. `https://notify.example.com`). Used to build webhook URLs shown in the UI. Also settable from the Settings page. |
|
|
| `ALLOW_PRIVATE_URLS` | unset | Set to `1` to allow requests to RFC1918 / loopback / link-local hosts (homelab scenario: Immich/Gitea on the same LAN). **Do not enable on a publicly exposed instance.** |
|
|
|
|
Auth & tokens:
|
|
|
|
| Variable | Default | Description |
|
|
| -------- | ------- | ----------- |
|
|
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | Lifetime of access JWTs. |
|
|
| `REFRESH_TOKEN_EXPIRE_DAYS` | `30` | Lifetime of refresh tokens. |
|
|
| `JWT_ISSUER` | `notify-bridge` | `iss` claim. |
|
|
| `JWT_AUDIENCE` | `notify-bridge-api` | `aud` claim. |
|
|
|
|
Logging (all are also live-editable in the Settings page, except `log_format`):
|
|
|
|
| Variable | Default | Description |
|
|
| -------- | ------- | ----------- |
|
|
| `LOG_LEVEL` | `INFO` | Root level: `DEBUG` / `INFO` / `WARNING` / `ERROR`. |
|
|
| `LOG_FORMAT` | `text` | `text` or `json`. Switching requires a restart. |
|
|
| `LOG_LEVELS` | — | Per-module overrides, e.g. `notify_bridge_core.notifications.telegram.client=DEBUG,sqlalchemy.engine=INFO`. |
|
|
|
|
Retention & maintenance:
|
|
|
|
| Variable | Default | Description |
|
|
| -------- | ------- | ----------- |
|
|
| `EVENT_LOG_RETENTION_DAYS` | `30` | Days of `event_log` history to keep. `0` disables the retention job. |
|
|
| `PRE_MIGRATE_SNAPSHOT_KEEP` | `5` | Number of pre-migration DB snapshots to keep in `<DATA_DIR>/backups/`. `0` disables snapshotting. |
|
|
| `GRACEFUL_SHUTDOWN_SECONDS` | `60` | Time to wait for in-flight requests / scheduler jobs on SIGTERM before force-killing. |
|
|
|
|
Integrations & misc:
|
|
|
|
| Variable | Default | Description |
|
|
| -------- | ------- | ----------- |
|
|
| `TELEGRAM_WEBHOOK_SECRET` | — | Shared secret for Telegram bot webhooks. Also settable from the Settings page. |
|
|
| `TIMEZONE` | `UTC` | IANA timezone (e.g. `Europe/Warsaw`) used by the scheduler. Also settable from the Settings page. |
|
|
| `STATIC_DIR` | `/app/static` (in Docker) | Frontend static files directory. The Docker image sets this; don't override unless you're running outside the image. |
|
|
| `SUPERVISED` | auto-detect | Set to `1` to tell the backup endpoint that an external supervisor will restart the process. |
|
|
|
|
### Docker Compose
|
|
|
|
```yaml
|
|
services:
|
|
notify-bridge:
|
|
image: git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge:latest
|
|
container_name: notify-bridge
|
|
restart: unless-stopped
|
|
ports:
|
|
- "8420:8420"
|
|
volumes:
|
|
- notify-bridge-data:/data
|
|
environment:
|
|
# REQUIRED — any 32+ byte random string. `openssl rand -hex 32` is one way.
|
|
- NOTIFY_BRIDGE_SECRET_KEY=${NOTIFY_BRIDGE_SECRET_KEY:?Set NOTIFY_BRIDGE_SECRET_KEY (min 32 chars)}
|
|
# Comma-separated list of allowed browser origins. Wildcard `*` is
|
|
# rejected on startup because credentials are enabled.
|
|
- NOTIFY_BRIDGE_CORS_ALLOWED_ORIGINS=${NOTIFY_BRIDGE_CORS_ALLOWED_ORIGINS:-http://localhost:8420}
|
|
# Trusted proxy IPs whose X-Forwarded-For / X-Forwarded-Proto we honor.
|
|
# Set this to your reverse proxy's IP (e.g. 172.17.0.1 for the default
|
|
# docker bridge, or `*` only if the container is NOT reachable from the
|
|
# public internet).
|
|
- NOTIFY_BRIDGE_FORWARDED_ALLOW_IPS=${NOTIFY_BRIDGE_FORWARDED_ALLOW_IPS:-127.0.0.1}
|
|
# Opt-in SSRF bypass for private/loopback/link-local hosts (homelab
|
|
# scenario — tracking an Immich/Gitea instance on the same LAN). DO NOT
|
|
# enable on a publicly exposed instance.
|
|
# - NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1
|
|
healthcheck:
|
|
# Use /api/ready (not /api/health) so the container is only reported
|
|
# healthy after migrations and the scheduler finish booting.
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8420/api/ready', timeout=3)"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
mem_limit: 512m
|
|
cpus: 1.0
|
|
pids_limit: 256
|
|
|
|
volumes:
|
|
notify-bridge-data:
|
|
```
|
|
|
|
A ready-to-use `docker-compose.yml` lives at the repo root.
|
|
|
|
### Health & Readiness
|
|
|
|
- `GET /api/health` — process is up. Use for liveness probes.
|
|
- `GET /api/ready` — migrations + scheduler have booted. Use for readiness probes and Docker `HEALTHCHECK` (as the compose example above does).
|
|
|
|
## Quick Start (Development)
|
|
|
|
```bash
|
|
# Backend
|
|
cd packages/server
|
|
pip install -e .
|
|
NOTIFY_BRIDGE_DATA_DIR=./test-data NOTIFY_BRIDGE_SECRET_KEY=your-secret-key-min-32chars \
|
|
python -m uvicorn notify_bridge_server.main:app --host 0.0.0.0 --port 8420
|
|
|
|
# Frontend
|
|
cd frontend
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
## Supported Providers
|
|
|
|
- **Immich** — Photo/video server with album change detection (polling)
|
|
- **Gitea** — Git server with push / issue / PR / release events (webhook)
|
|
- **Planka** — Kanban board with card / list / board events (webhook)
|
|
- **NUT** — Network UPS Tools for battery / power events (polling)
|
|
- **Google Photos** — Album change detection (polling)
|
|
- **Generic Webhook** — Catch arbitrary JSON payloads and route them via templates (webhook)
|
|
- **Scheduler** — Internal provider for time-based scheduled messages
|
|
|
|
## Supported Notification Targets
|
|
|
|
- **Telegram** — Bot API with rich formatting, media groups, and inline commands
|
|
- **Discord** — Webhook-based delivery with embeds
|
|
- **Slack** — Incoming webhooks with Block Kit formatting
|
|
- **Matrix** — Homeserver delivery with HTML formatting
|
|
- **ntfy** — Self-hostable push notifications
|
|
- **Email** — SMTP with HTML / plain-text templates
|
|
- **Generic Webhook** — POST custom JSON payloads to any URL
|
|
|
|
## Bot Commands
|
|
|
|
Telegram bots can serve interactive commands per provider. All commands use
|
|
Jinja2 templates that you can customize from the **Command Templates** page.
|
|
|
|
| Provider | Commands |
|
|
| -------- | -------- |
|
|
| Immich | `/status` `/albums` `/events` `/summary` `/latest` `/memory` `/random` `/search` `/find` `/person` `/place` `/favorites` `/people` `/help` |
|
|
| Gitea | `/status` `/repos` `/issues` `/prs` `/commits` `/help` |
|
|
| Planka | `/status` `/boards` `/cards` `/lists` `/help` |
|
|
| NUT | `/status` `/devices` `/battery` `/help` |
|
|
| Google Photos | `/status` `/albums` `/latest` `/search` `/random` `/help` |
|
|
| Generic Webhook | `/status` `/help` |
|
|
|
|
Every provider also responds to `/start`, and rate-limit / empty-result
|
|
fallback messages are templated as well.
|
|
|
|
## Smart Actions
|
|
|
|
Beyond notifications, providers can run **actions** against the source service.
|
|
Currently implemented:
|
|
|
|
- **Immich — Auto-Organize** — Automatically sort newly-detected assets into
|
|
albums based on configurable rules. Each rule combines criteria (people in
|
|
the photo, search query, favorites, date range) with a target album, and can
|
|
create the album if it doesn't exist. Supports dry-run mode for previewing
|
|
what would move before committing.
|