# 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 four 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.
- `combined`: Phoenix Endpoint and the full Oban queues in one BEAM VM for a
  lower-overhead single-server deployment.
- `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.
The worker and combined roles start 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

The Compose launcher selects two independent dimensions from the ignored
environment:

- `APP_TOPOLOGY=split` starts Traefik plus independently scalable web and
  worker replicas. Normal development defaults to 2 web and 2 worker replicas.
- `APP_TOPOLOGY=compact` starts one combined Phoenix+Oban VM and publishes its
  port directly to the host reverse proxy.
- `DATABASE_MODE=container` starts one project-owned PostGIS container.
- `DATABASE_MODE=external` removes the PostGIS service from the active Compose
  model and connects migrate/application roles through `DATABASE_URL`.

The normal split development combination 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 is never started by Docker Compose. `DEPLOYMENT_TARGET=kubernetes`
is handled explicitly by `scripts/deploy-up.sh`; `KUBERNETES_MODE=kind` is the
local verification path and `KUBERNETES_MODE=helm` is the external-cluster
path.

### 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, email provider, 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/)
