8.2 KiB
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.
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. A JavaScript bridge 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, authentication, social identities, privacy preferences, blocks, and roles.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
webservice 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.
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.
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, 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:
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.