Bulwark Webmail
# Bulwark Relay Push notification relay for Bulwark Webmail. Terminates JMAP `PushSubscription` pushes from the user's mail server and forwards them as Firebase Cloud Messaging, Web Push or [UnifiedPush](https://unifiedpush.org) pings so the mobile app (or PWA) wakes up and fetches new mail over its own JMAP connection. Designed so self-hosters don't need their own Firebase project, a single hosted instance serves every Bulwark client that opts in. The relay never sees mail content: only opaque FCM tokens, state-id hashes, and timing. ## Endpoints | Method | Path | Purpose | | --- | --- | --- | | `POST` | `/api/push/register` | Mobile app stores its FCM token against an opaque `subscriptionId` | | `POST` | `/api/push/register/web` | PWA stores a Web Push subscription (`endpoint` + `keys`) against an opaque `subscriptionId` | | `POST` | `/api/push/register/unifiedpush` | Mobile app stores a [UnifiedPush](https://unifiedpush.org) endpoint (`endpoint` + optional Web Push `keys`) against an opaque `subscriptionId`. Delivery is encrypted Web Push when keys are present (connector >= 3.0), a plain POST of the ping otherwise. Works without VAPID configured - distributors don't require it | | `DELETE` | `/api/push/register/:id` | Tear down mapping (logout / uninstall) | | `GET` | `/api/push/verify/:id` | Poll for the JMAP `PushVerification` code | | `GET` | `/api/push/active/:id` | Liveness probe. Returns `{ active }` if the subscription has forwarded a push (or was just registered), `404` if unknown. Clients use it to reap dead leftover subscriptions without touching live ones | | `POST` | `/api/push/jmap/:id` | JMAP server posts `PushVerification`, `StateChange` or `EmailPush` (draft-ietf-jmap-emailpush) here, and the relay dispatches FCM or Web Push depending on the stored record. For `EmailPush` only the message ids are forwarded, never any other property the server included | | `GET` | `/api/push/vapid-public-key` | Returns the relay's VAPID public key so browsers can subscribe | | `GET` | `/api/health` | Liveness probe | ## What it stores Per `subscriptionId`: an FCM token (mobile), a Web Push subscription (`endpoint` + `p256dh`/`auth` keys for the PWA), or a UnifiedPush endpoint (mobile without Google Play services), an optional one-shot verification code, an optional free-form `accountLabel` (max 120 chars), and timestamps. No user identity, no server URL, no mail content. Subscriptions older than 90 days without traffic are evicted automatically. ## Run locally ```sh npm install echo '{...}' > data/fcm-service-account.json # Firebase service account JSON npm run dev ``` ## Run in Docker ```sh mkdir -p data cp path/to/fcm-service-account.json data/ docker compose up -d ``` ## Environment | Var | Default | Notes | | --- | --- | --- | | `PORT` | `3003` | HTTP listen port | | `HOST` | `0.0.0.0` | | | `PUSH_DATA_DIR` | `./data` | Where `subscriptions.json` and the FCM key live | | `FCM_SERVICE_ACCOUNT_JSON` | unset | Either the full JSON inline or an absolute path; falls back to `$PUSH_DATA_DIR/fcm-service-account.json` | | `VAPID_PUBLIC_KEY` | unset | Base64url-encoded P-256 public key for Web Push. Generate with `npx web-push generate-vapid-keys` | | `VAPID_PRIVATE_KEY` | unset | Matching private key. Web Push is disabled if either VAPID var is missing | | `VAPID_SUBJECT` | `mailto:postmaster@localhost` | `mailto:` or `https:` contact the push services can reach if the relay misbehaves (RFC 8292) | ## License Licensed under the GNU Affero General Public License v3.0 only. See [LICENSE](LICENSE).