who_need_help/docs/architecture.md

210 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/)