# Architecture Status: target architecture for the first working release. ## System shape Who Need Help is a modular Phoenix application rather than a collection of premature microservices. It produces one immutable image with three runtime roles: - `web`: Phoenix Endpoint, LiveView, PubSub, Presence, and a producerless Oban client for atomic job insertion. - `worker`: Oban queues and scheduled jobs; no public HTTP listener. - `migrate`: a one-shot database migration command before rollout. PostgreSQL with PostGIS is the system of record. User-visible writes are committed to PostgreSQL before a PubSub notification is broadcast. This makes realtime delivery recoverable: after a reconnect, the client reads authoritative state from the database. ```text browser/PWA or Android WebView │ ▼ Traefik / Kubernetes Service │ ├── web replica A ─┐ └── web replica B ─┼── PostgreSQL + PostGIS │ worker A/B ─────┤ └── Oban jobs web and worker nodes form a BEAM cluster for distributed PubSub/Presence ``` The Android module is a thin same-origin WebView shell with one native foreground location service. Its debug origin is supplied at build time from the repository's ignored `.env`; release builds require an explicit HTTPS origin. Authentication cookies, LiveView WebSockets, MapLibre, and private chat use the same Phoenix application as the browser. An origin-restricted `WebViewCompat` message listener accepts tracking commands only from the trusted main frame and starts the native service only from the visible Activity; the service posts the current point through CSRF-protected, participant-authorized same-origin routes and shows a persistent notification with Stop. Production signing and distribution are separate operational work and are not represented as complete. ## Application boundaries - `Accounts`: users, email/password authentication, private authentication identities, public social identities, privacy preferences, blocks, and roles. - `GoogleAuth`: optional Google OpenID Connect boundary for registration, sign-in, and account linking. It uses state, nonce, and PKCE, returns only a normalized verified-email identity, and does not expose provider tokens to controllers or persistence. - `SocialOAuth`: optional provider boundary for verified social linking. The GitHub adapter uses OAuth state and PKCE, returns only normalized identity attributes to the controller, and does not expose the provider token to the persistence layer. - `Catalog`: category tree, proposals, votes, moderation decisions, and translated labels. - `Help`: requests, assignments, state transitions, handover codes, and completion evidence. - `Activities`: social plans, organizer-approved participants, private group messages, capacity-safe joins, and participant-scoped meeting coordinates. - `Messaging`: match chat and delivery receipts. - `Tracking`: consent-driven sharing sessions, ephemeral current positions, and derived proximity signals. - `Trust`: reviews, reports, blocks, leaderboard/reputation projections, abuse signals, moderator audit events, and shared rate-limit policies. - `Push`: privacy-safe product event construction, unique durable Oban jobs, and a provider-neutral delivery adapter. Current events cover request acceptance and new matched-chat messages. Contexts normally call each other through public functions. A small number of documented trust-and-safety transactions update related schemas together when atomic cleanup or moderation requires it; those operations stay in context functions rather than controllers or LiveViews. ## Realtime and clustering Phoenix PubSub and Presence handle transient fan-out. Phoenix's generated `dns_cluster` dependency discovers nodes using DNS polling: - Compose nodes discover the `web` service on the shared internal network; the internal-only `cluster-web` alias and `CLUSTER_INTERFACE=eth0` keep node names on that network even when the container also has ingress or egress interfaces. Workers join through those nodes and distributed Erlang forms the full mesh. - Kubernetes uses a headless service. No sticky session is required for authenticated requests. Session cookies are signed by a shared secret. Uploads, if later introduced, must go to shared object storage rather than a container filesystem. Web and migrate application processes start a producerless Oban client with no queues, plugins, or peer leadership so transactions can insert unique jobs. Only the worker role starts queue consumers and scheduled-job plugins. PostgreSQL coordinates queues and leadership, so no Redis dependency is introduced. The worker runs only the queues used by product code: `maintenance` for expiry/probes and `push` for provider-neutral delivery. Their per-worker concurrency is configured independently; no unused default queue is started. ## Geospatial data Locations are stored as WGS84 coordinates through the PostGIS Ecto extension. Current movement and proximity evidence is derived in the application from two current points and their client-reported accuracy envelopes; no route is retained. Browser updates stop with the page. Android updates may continue while minimized only through the user-started foreground service and its visible notification; the app does not request `ACCESS_BACKGROUND_LOCATION`. MapLibre GL JS renders the map through a small JavaScript hook. The tile/style URL and attribution are configuration, not hard-coded provider assumptions. Production operators must use a tile service whose policy and capacity fit the traffic; the public OpenStreetMap tile service is best-effort and has a usage policy, not an application backend. ## Data durability and retention - Requests, activities, membership decisions, messages, reviews, reports, and audit events are durable database records. - A tracking session stores only its current position while active, never a route history. - Stopping sharing, completing/cancelling a match, or blocking the counterpart deletes exact position records. Derived sample count, uncertainty-adjusted distance, movement timestamp, and proximity timestamp remain. - Secrets, exact coordinates, and private chat are excluded from local Codex moderation inputs. ## Deployment ### Docker Compose Normal development starts: - Traefik - 2 × web - 2 × worker - 1 × PostGIS - 1 × Mailpit Compose services have no fixed container names, enabling replicas. The project uses an isolated Compose project name and network. PostgreSQL is intentionally single-instance in the local profile; multiple uncoordinated containers would not create database high availability. ### Kubernetes The Helm chart contains separate web and worker Deployments, Services, a headless cluster-discovery Service, a migration Job, Secret interfaces, probes, disruption-aware rolling updates, and an ingress NetworkPolicy. The policy allows unrestricted Erlang distribution only between pods belonging to the same chart instance, exposes the configured HTTP listener, and isolates other inbound pod ports. Kubernetes enforces this only when the cluster's network plugin implements NetworkPolicy. It does not restrict egress because the production database, SMTP, OAuth, map, and push-provider destinations are not known yet; those rules must be added from verified deployment-specific addresses rather than invented in the chart. The chart does not invent CPU/memory limits or an HPA threshold before measurements exist. The bundled kind path is for reproducible local verification. A production database should be an independently operated PostgreSQL/PostGIS service with backups and a tested recovery procedure. ## Shared abuse counters Action buckets live in PostgreSQL and use an atomic upsert keyed by action, hashed scope, and aligned time window. This works across all web replicas without an in-memory or Redis singleton. Policies are supplied through `RATE_LIMIT_POLICIES_JSON`; no numeric product policy is compiled into the application. Expired buckets are pruned by the maintenance worker. The Compose and kind scripts finish by subscribing on one live BEAM node, broadcasting through a different connected node, and failing if the PubSub probe is not received. ## AI boundary AI is not in the critical request or safety path. An administrator can export a redacted batch of category proposals to the locally installed Codex CLI: ```text codex exec --ephemeral --sandbox read-only --output-schema … ``` The process uses the user's ChatGPT-authenticated local Codex session. It never uses an OpenAI API key, usage-based API billing, or an API fallback. The model may group duplicates and draft English, Ukrainian, and Russian labels; a human moderator applies every change. ## Versioning policy Exact image and package versions are pinned in the repository after successful build verification. Renovation is a deliberate change accompanied by tests, not an implicit `latest` pull. ## Primary references - [Phoenix 1.8 documentation](https://hexdocs.pm/phoenix/overview.html) - [Phoenix PubSub](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) - [Phoenix Presence](https://hexdocs.pm/phoenix/presence.html) - [Oban](https://hexdocs.pm/oban/Oban.html) - [DNSCluster](https://hexdocs.pm/dns_cluster/DNSCluster.html) - [PostGIS](https://postgis.net/documentation/) - [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) - [OpenStreetMap tile usage policy](https://operations.osmfoundation.org/policies/tiles/) - [W3C Geolocation](https://www.w3.org/TR/geolocation/)