# BirdWatch Relay Self-hosted bridge between [Frigate NVR](https://frigate.video) and the BirdWatch Android app. Forwards Frigate event webhooks to your phone via Firebase Cloud Messaging, with payloads end-to-end encrypted (NaCl sealed box, X25519 + XSalsa20-Poly1305) so Google sees ciphertext only. You run this on the same Linux machine as Frigate (your NVR). Source is AGPLv3. ## Quick install The interactive installer asks the questions, generates the secret, writes the config, builds the image, starts the container, and prints the exact strings to paste into Frigate and the BirdWatch app. ```bash git clone https://github.com/cyrilinait/birdwatch-relay.git cd birdwatch-relay ./setup.sh ``` The script needs Docker, Docker Compose v2, `openssl`, and `curl` — all present on a typical Frigate NVR. You'll be asked for two things during setup: 1. The path to your Firebase service-account JSON file (instructions below if you don't have one). 2. The hostname or LAN IP your phone will reach the relay at. That's it. The script handles the rest, then prints the URL to paste into the BirdWatch app and the webhook URL + `Authorization` header to add to Frigate's `config.yml`. ## Getting a Firebase service-account key (one time, ~5 minutes) The relay sends pushes via Google's Firebase Cloud Messaging. You need a free Firebase project to do that. Google sees only ciphertext and a device token — they cannot read the events. 1. Open https://console.firebase.google.com 2. Click **Add project**. Name it whatever you want (e.g. `birdwatch-home`). Skip Analytics if asked. 3. Once the project loads, click the gear icon (top-left) → **Project settings**. 4. Open the **Service accounts** tab. 5. Click **Generate new private key**. A `.json` file downloads — this is the file the installer will ask for. 6. Move it to your NVR (e.g. with `scp`). Anyone with this file can send pushes to your phones, so keep it private (the installer copies it to `./firebase-key.json` with mode 600 and the `.gitignore` excludes it). ## Running alongside an existing Frigate stack If your Frigate is already deployed via `docker-compose.yml`, you can merge the relay into the same stack instead of running it standalone. After running `./setup.sh` once to generate `.env`, edit your existing Frigate `docker-compose.yml` and add this service block: ```yaml birdwatch-relay: build: /path/to/birdwatch-relay container_name: birdwatch-relay restart: unless-stopped ports: - "8080:8080" volumes: - /path/to/birdwatch-relay/data:/data - /path/to/birdwatch-relay/firebase-key.json:/secrets/firebase-key.json:ro environment: RELAY_FIREBASE_KEY_PATH: /secrets/firebase-key.json RELAY_WEBHOOK_SECRET: ${RELAY_WEBHOOK_SECRET} RELAY_DB_PATH: /data/relay.db RELAY_PORT: "8080" ``` Replace `/path/to/birdwatch-relay` with the absolute path of this clone. Add `RELAY_WEBHOOK_SECRET=...` to your stack's `.env` (copy the value from `birdwatch-relay/.env`). Then `docker compose up -d`. The advantage: one stack, one place to start/stop, the relay starts and stops with Frigate. ## What ends up where (the data picture) | Where | What lives there | |-------|------------------| | Your NVR | Container, SQLite (`./data/relay.db`), Firebase key (`./firebase-key.json`), webhook secret (`./.env`). All on your hardware. | | Your phone | The X25519 private key (Android Keystore-protected, never leaves the device). | | Google FCM | Your phone's FCM token + the encrypted blob + delivery timestamps. No event content. | | Anywhere else (cyrilina.it, etc.) | Nothing. There is no third-party endpoint in the data path. | ## Endpoints - `POST /v1/devices` — phones register here. Body: `{"device_id":"...", "fcm_token":"...", "public_key":""}`. Returns 204. - `POST /v1/webhook` — Frigate sends events here. `Authorization: Bearer `. Body: any JSON ≤ 64 KB. Returns 202. - `GET /v1/health` — liveness probe. Returns `ok` / 200. ## Manual install (if you don't want to use setup.sh) ```bash # 1. Generate a webhook secret. openssl rand -hex 32 > /tmp/secret.txt # 2. Place your Firebase key. cp /path/to/your-firebase-key.json ./firebase-key.json chmod 600 ./firebase-key.json # 3. Write .env. echo "RELAY_WEBHOOK_SECRET=$(cat /tmp/secret.txt)" > .env chmod 600 .env # 4. Copy the example compose file and adjust ports if you want. cp docker-compose.example.yml docker-compose.yml # 5. Build and start. docker compose up -d --build # 6. Verify. curl http://localhost:8080/v1/health # should print: ok ``` Webhook URL for Frigate: `http://:8080/v1/webhook` with header `Authorization: Bearer `. ## Troubleshooting **Container won't start.** `docker compose logs relay`. Most common: missing `RELAY_FIREBASE_KEY_PATH` or `RELAY_WEBHOOK_SECRET`, or the secret is shorter than 32 characters. **Webhook returns 401.** The `Authorization: Bearer …` value Frigate is sending doesn't match `RELAY_WEBHOOK_SECRET` in `.env`. Re-paste, restart Frigate. **Relay logs `webhook fanout sent=N` but no notification on phone.** The push reached FCM. Check the BirdWatch app has notifications enabled at the OS level, and that battery-saver mode is ON inside BirdWatch. **Notification arrives but says "Encrypted message" / can't decrypt.** Phone's private key doesn't match the public key registered with the relay. Toggle BirdWatch's battery-saver switch off then on to re-register. **`firebase init failed: invalid_grant`.** The Firebase service-account JSON is corrupted or wrong file. Re-download from Firebase Console → Project Settings → Service accounts. **Re-run setup.** `./setup.sh` is idempotent. It backs up the previous `.env` and `docker-compose.yml` to `*.bak` before writing new ones. ## Security model - **The relay holds your Firebase admin key.** If it leaks, an attacker can send pushes (but not read past notifications — those are encrypted to your phone). `.gitignore` excludes the file. Keep it off public storage. - **The webhook secret is the only thing stopping random people on your network from sending fake events.** It's 64 hex characters by default (256 bits). Compared with constant-time comparison server-side. Rotate if exposed. - **Distroless static container, nonroot user, no shell, no package manager.** Minimal attack surface inside the container. - **TLS** — the relay does not terminate TLS itself. For LAN-only deployments that's fine. If you expose the relay to the public internet (e.g. so phones can receive notifications away from home Wi-Fi), put a TLS reverse proxy in front (Caddy, Traefik, nginx) and use `https://` in the BirdWatch app and Frigate. ## License AGPL-3.0-or-later. See [`LICENSE`](LICENSE). Source must accompany any deployment that serves users over a network — that's the AGPL difference. ## Contributing This relay is a small dedicated service. Patches welcome via PR. Before publishing changes, run the security and optimize agents per `feedback_security_optimize_before_ship.md` in the BirdWatch project memory.