172 lines
6.9 KiB
Markdown
172 lines
6.9 KiB
Markdown
# 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/)
|