diff --git a/android/PLAN.md b/android/PLAN.md index bb50044..9020d83 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -169,9 +169,12 @@ none (keeps §11's zero-interaction promise). registration and every publish validate it is HTTPS and its origin is in the shard's ntfy allow-set (`NTFY_BASE_URL` / `NTFY_ALLOWED_ORIGINS`), rejecting loopback/private hosts. - **ntfy** added to `website/docker-compose.yml` as a pinned upstream image with a committed - declarative `./ntfy/server.yml` and named volume, **no published host port** (reached via the - reverse proxy; internal-only for the publisher), anonymous read-write to unguessable topics (no - per-user accounts — safe because tickles are content-free). + declarative `./ntfy/server.yml` and named volume, **published on a host port** (`NTFY_HOST_PORT`, + default `2586` → container `:80`) so the public reverse proxy — which runs *outside* the compose + network — can forward the notification subdomain to it, anonymous read-write to unguessable topics + (no per-user accounts — safe because tickles are content-free). Both the app (SSE subscribe) and the + backend (POSTing tickles to registered device endpoints) reach ntfy on that public origin, so all + ntfy traffic transits the proxy — there is no separate internal publish port. **Part 2 — the Android app — ✅ LANDED** (2026-07-20, `RunicGateway/Android-app#15` + a small `RunicGateway/website#79` settings addition + this docs PR). Built exactly to the plan below, with @@ -911,8 +914,11 @@ first release. Users **opt in per stream**: nothing is pushed unless subscribed. path/tag at implementation. - **All config is declarative** — a committed `ntfy` config file and/or `NTFY_*` env vars baked into compose. No `docker exec`, no interactive `ntfy user add`, no post-deploy manual steps. Bringing the - stack up provisions a working push relay. Reachable to devices via the existing reverse proxy on its - own hostname/path; internal-only for the backend publisher. + stack up provisions a working push relay. Reachable via the existing reverse proxy on its own + hostname/path — the container publishes `:80` on a host port (`NTFY_HOST_PORT`, default `2586`) + because the proxy runs *outside* the compose network and can only reach a service through a + published host port (the same reason `app` publishes `3000`). Both devices and the backend publisher + reach ntfy on that public origin. - **No per-user ntfy accounts to administer.** The security model (below) removes the need for ntfy ACL provisioning, which is exactly what keeps setup interaction-free. ntfy topics are the random, unguessable endpoints UnifiedPush hands out; the backend treats ntfy as an **untrusted relay**. @@ -1002,8 +1008,10 @@ it is a UX convenience, not a v1 requirement — deferred at M3, descoped at M6; See [`APP_LINKS.md`](./APP_LINKS.md). - ntfy: exact upstream image + pinned tag (Part-1 landed the compose service — confirm the tag), and its reverse-proxy hostname/path. The hostname must land in `NTFY_ALLOWED_ORIGINS` before M7 Part 2 is - end-to-end testable (the app registers an endpoint on that origin; the SSRF guard rejects others). No - backend publish token — **decided** (the content-free-tickle design does not require one; optional + end-to-end testable (the app registers an endpoint on that origin; the SSRF guard rejects others). The + reverse proxy forwards that hostname to the ntfy container's published host port (`NTFY_HOST_PORT`, + default `2586`) — the container publishes `:80` because the proxy runs outside the compose network. + No backend publish token — **decided** (the content-free-tickle design does not require one; optional `NTFY_PUBLISH_TOKEN` is honored if ever set). - FCM flavor: build it for the Play release or ship Play on UnifiedPush too? Decide at M8. (The M7 Part 2 `PushTransport` seam keeps this swap cheap.) diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index 3956299..b7e892f 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -511,11 +511,13 @@ subsystem (`[server]`, `[http]`, `[db]`, `[auth]`, `[admin]`, `[ratelimit]`, … `env_file: .env`, `DB_HOST=db`, `depends_on: db (healthy)`, volume `uploads:/app/uploads`, `ports: "3000:3000"` — **binds 0.0.0.0** (no `127.0.0.1:` prefix) so Pangolin reaches it. - `ntfy` (M7): pinned upstream `binwiederhier/ntfy` image, declarative config only - (`./ntfy/server.yml` mounted `:ro` + `NTFY_BASE_URL`), volume `ntfydata:/var/lib/ntfy`, **no - published host port** — devices reach it via the reverse proxy; the backend publisher reaches it - over the private compose network. Anonymous read-write to unguessable topics (no accounts to - provision) — safe because pushes are content-free tickles. Bringing the stack up provisions a - working push relay with **zero interactive setup**. + (`./ntfy/server.yml` mounted `:ro` + `NTFY_BASE_URL`), volume `ntfydata:/var/lib/ntfy`, + **publishes `:80` on a host port** (`${NTFY_HOST_PORT:-2586}:80`, binds 0.0.0.0) so Pangolin — which + runs outside the compose network — can forward the notification subdomain to it, the same reason + `app` publishes `3000`. Both devices (SSE subscribe) and the backend publisher (POSTing tickles to + registered device endpoints) reach ntfy on that public origin. Anonymous read-write to unguessable + topics (no accounts to provision) — safe because pushes are content-free tickles. Bringing the stack + up provisions a working push relay with **zero interactive setup**. - Volumes: `dbdata`, `uploads`, `ntfydata`. Express listens on `0.0.0.0:${PORT||3000}`. Pangolin terminates TLS and proxies to `app`. @@ -548,6 +550,9 @@ NTFY_BASE_URL=https://ntfy.example.com # shows push as unavailable for the shard. NTFY_PUBLIC_URL=https://ntfy.example.com NTFY_ALLOWED_ORIGINS=https://ntfy.example.com +# Host port the ntfy container publishes :80 on (default 2586); the reverse proxy +# forwards the notification subdomain to host:NTFY_HOST_PORT. Change on a conflict. +NTFY_HOST_PORT=2586 ``` `.gitignore`: `node_modules/`, `.env`, `_reference/`, `client/dist/`, `uploads/`.