who_need_help/docs/architecture.md

172 lines
6.9 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, and Presence.
- `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 rather than a separate
API client. 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, private chat, and foreground
geolocation therefore use the same Phoenix application paths and authorization
rules as the browser. Production signing and distribution are separate
operational work and are not represented as complete.
## Application boundaries
- `Accounts`: users, authentication, social identities, privacy preferences,
blocks, and roles.
- `Catalog`: category tree, proposals, votes, moderation decisions, and
translated labels.
- `Help`: requests, assignments, state transitions, handover codes, and
completion evidence.
- `Messaging`: match chat and delivery receipts.
- `Tracking`: foreground sharing sessions, current positions, and derived
proximity signals.
- `Trust`: reviews, reports, blocks, leaderboard/reputation projections,
abuse signals, moderator audit events, and shared rate-limit policies.
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;
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.
Oban is enabled only for the worker role. PostgreSQL coordinates queues and
leadership, so no Redis dependency is introduced.
## 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 browser-reported accuracy envelopes; no route is
retained. 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, state transitions, 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,
and disruption-aware rolling updates. It 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/)