476 lines
20 KiB
Markdown
476 lines
20 KiB
Markdown
# Who Need Help
|
|
|
|
Who Need Help is a working mutual-aid MVP for urgent, local, voluntary help.
|
|
The first priority is pickup and delivery of a legal medicine that has already
|
|
been purchased or reserved. The same urgent-help flow also supports safe,
|
|
non-emergency fuel delivery, wheel help, bicycle and motorcycle problems,
|
|
vehicle breakdowns, and practical support after a secured road incident.
|
|
|
|
It is not an emergency or medical service, does not prescribe or sell medicine,
|
|
and does not process payments. A helper may publish an optional external
|
|
thank-you link; money goes directly between users outside the platform.
|
|
|
|
## What is implemented
|
|
|
|
- Phoenix 1.8 LiveView application with email magic-link/password auth and 18+
|
|
self-attestation.
|
|
- Data-driven, translated category tree with validated per-category fields,
|
|
seven selectable urgent/roadside scenarios, community proposals/votes, and
|
|
human approve/reject/merge tools.
|
|
- Separate social Activity mode for coffee, cinema, walks, and hikes, with
|
|
organizer-approved membership, capacity-safe joins, private group chat, and
|
|
exact coordinates visible only to approved participants. Activities never
|
|
affect urgent-helper reputation; Activity and Activity-message reports expose
|
|
only the linked evidence to audited moderators.
|
|
- Request lifecycle: `open → matched → in_progress → completed`, plus cancel
|
|
and expiry paths.
|
|
- PostgreSQL/PostGIS locations, MapLibre map, private matched chat, Phoenix
|
|
PubSub/Presence, and optional consent-driven live location sharing.
|
|
- Handover code plus both-party confirmation before verified completion.
|
|
- Double-blind reviews, public trust summaries, and a helper leaderboard that
|
|
prioritizes unique location-supported and handover-verified counterparts
|
|
before raw totals.
|
|
- Bidirectional discovery blocks, scoped reports, account/request/category
|
|
moderation, abuse-signal review, and audited moderator access to only the
|
|
conversation linked by a report.
|
|
- Optional GPS evidence derived from browser accuracy envelopes. Raw current
|
|
positions are deleted on stop, terminal match state, or participant block.
|
|
- PostgreSQL-backed cross-replica action-limit policies configured by the
|
|
operator. No unapproved numeric thresholds are enabled by default.
|
|
- Public manual social links, always marked unverified, plus optional verified
|
|
GitHub linking through a state- and PKCE-protected OAuth flow. Provider access
|
|
tokens are not stored.
|
|
- EN/UK/RU UI foundation and installable PWA metadata/service worker.
|
|
- Native Android WebView client with the same authenticated LiveView, map,
|
|
private chat, and a user-started location foreground service. Its persistent
|
|
notification exposes Stop, it continues while the Activity is minimized, and
|
|
it retains only the current point. Reproducible Docker targets export
|
|
distinct local and public-staging debug APKs; production signing and store
|
|
publication are not configured.
|
|
- Local, advisory Codex category review through the user's ChatGPT-authenticated
|
|
Codex CLI. It receives a PII-free export and never writes to the database.
|
|
- One immutable release image with `web`, `worker`, and one-shot `migrate`
|
|
roles.
|
|
- Docker Compose and Helm/kind deployment paths with 2 web and 2 worker
|
|
replicas by default.
|
|
|
|
Additional social providers, background PWA or unattended location tracking,
|
|
platform payments, production Android signing/store publication, iOS,
|
|
automatic punitive fraud decisions, and jurisdiction-specific public-launch
|
|
policies are deliberately not claimed as complete.
|
|
|
|
## Fast start with Docker Compose
|
|
|
|
Prerequisite: Docker with the Compose plugin.
|
|
|
|
```bash
|
|
./scripts/compose-up.sh
|
|
```
|
|
|
|
Open:
|
|
|
|
- app: <http://localhost:4010>
|
|
- local email inbox: <http://localhost:8027>
|
|
|
|
Compose starts Traefik, PostGIS, Mailpit, a migration runner, 2 web replicas,
|
|
and 2 Oban worker replicas. It waits for readiness and verifies a PubSub message
|
|
broadcast from a different BEAM node. Registration emails appear in Mailpit.
|
|
|
|
Inspect the exact state:
|
|
|
|
```bash
|
|
docker compose -p who_need_help ps -a
|
|
docker compose -p who_need_help logs -f web worker
|
|
./scripts/verify-realtime-cluster.sh compose
|
|
./scripts/verify-beam-runtime.sh compose
|
|
```
|
|
|
|
`ERLANG_PORT_LIMIT` defaults to the normal OTP port limit of `65536`. Keeping
|
|
it explicit prevents a container runtime's unusually large `nofile` limit from
|
|
making every BEAM instance preallocate a multi-gigabyte port table. The runtime
|
|
check above reads every running web/worker VM and fails if its effective limit
|
|
differs from the configured value. Increase it only from measured concurrent
|
|
file, socket, and driver requirements.
|
|
|
|
Create and restore-test a database backup without restoring over the source:
|
|
|
|
```bash
|
|
backup_output=$(./scripts/backup-compose.sh)
|
|
backup_path=$(printf '%s\n' "$backup_output" | sed -n 's/^Backup: //p')
|
|
./scripts/restore-drill-compose.sh "$backup_path"
|
|
```
|
|
|
|
Rehearse the complete current release against that exact backup without
|
|
changing the source Compose project:
|
|
|
|
```bash
|
|
./scripts/upgrade-rehearsal-compose.sh "$backup_path"
|
|
```
|
|
|
|
The rehearsal uses a uniquely named Compose project and database, applies the
|
|
current migrations, starts 2 web and 2 worker replicas, checks the configured
|
|
public-origin proxy behavior and cross-node PubSub, compares application-table
|
|
counts, and removes its containers, networks, volume, and one-run image.
|
|
|
|
Verify that the tracked Git revision can deploy from a clean directory with a
|
|
new environment, independent generated secrets, dynamic ports, and no
|
|
pre-existing image or volume:
|
|
|
|
```bash
|
|
./scripts/clean-deploy-verify.sh
|
|
```
|
|
|
|
The drill starts an isolated 2-web/2-worker Compose project, verifies current
|
|
and repeated migrations, HTTP and Mailpit routing, the four-node BEAM cluster,
|
|
and cross-replica PubSub, then removes its exact project, volumes, image, and
|
|
temporary tracked-file archive. It does not read or change the ordinary
|
|
Compose `.env` or database.
|
|
|
|
For the reproducible encrypted S3-compatible drill, first start the isolated
|
|
load project and then run:
|
|
|
|
```bash
|
|
./scripts/load-stack-up.sh
|
|
./scripts/backup-s3-drill.sh local-encrypted-backup
|
|
```
|
|
|
|
The command generates MinIO and Restic secrets only in ignored mode-`0600`
|
|
`.env.load`, streams `pg_dump` directly into an encrypted Restic repository,
|
|
restores it into a new temporary database, checks corruption and interruption
|
|
failure paths, removes those temporary buckets, and retains the successful
|
|
encrypted bucket in local MinIO. It never writes a plaintext dump to the host.
|
|
|
|
Exercise the real OAuth/SMTP protocol clients and the provider-neutral push
|
|
boundary entirely inside an isolated Docker network:
|
|
|
|
```bash
|
|
./scripts/external-boundaries-run.sh local-boundaries
|
|
```
|
|
|
|
The command publishes no host ports, generates independent one-run credentials
|
|
in an ignored mode-`0600` file, verifies success, rejection, retry, replay, and
|
|
timeout paths, retains only non-secret evidence, and removes its exact Compose
|
|
project, database volume, and images. It also runs two Oban worker replicas and
|
|
verifies that request acceptance and new-chat events are delivered from their
|
|
real domain transactions, including an Oban retry and pre-transport replay
|
|
deduplication. The adapter is not presented as an FCM or APNs implementation;
|
|
an external provider must resolve the stable user recipient to registered
|
|
devices.
|
|
|
|
The commands, boundaries, and unclaimed production properties are documented
|
|
in [the operations runbook](docs/operations.md).
|
|
|
|
Local defaults are intentionally limited to local development. Copy
|
|
`.env.example` to `.env` and replace every secret before any public deployment.
|
|
Generate independent values with `mix phx.gen.secret`; do not reuse the session
|
|
secret as the BEAM cookie or handover secret. External Helm deployments must
|
|
set `app.host`, `app.scheme`, and `app.urlPort` to the public URL used in email
|
|
links, and must set `app.mapTileUrl` to a tile service whose policy and capacity
|
|
fit the deployment.
|
|
|
|
Rotate all local application secrets and the existing local PostgreSQL role
|
|
without printing the generated values:
|
|
|
|
```bash
|
|
./scripts/rotate-local-secrets.sh
|
|
docker compose up -d --wait
|
|
```
|
|
|
|
This preserves the named PostgreSQL volume, but invalidates existing browser
|
|
sessions and changes handover codes for active local requests.
|
|
|
|
### Optional verified GitHub linking
|
|
|
|
Create a GitHub OAuth App with this exact callback URL for the active public
|
|
origin:
|
|
|
|
```text
|
|
https://YOUR_PHX_HOST/auth/social/github/callback
|
|
```
|
|
|
|
Set both `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET` in the
|
|
ignored `.env` or deployment Secret, then recreate the application containers.
|
|
If both values are empty, the feature stays disabled and the profile explains
|
|
that state. A partial pair is rejected at startup. The flow requests only the
|
|
public GitHub identity, protects callbacks with state and PKCE, binds the flow
|
|
to the initiating signed-in user, and never persists the provider access token.
|
|
The optional `GITHUB_OAUTH_*_URL` and HTTP timeout variables exist only so the
|
|
isolated boundary drill can use its internal mock. Leave them empty for the
|
|
official GitHub endpoints and Req defaults.
|
|
|
|
## Tests
|
|
|
|
The reproducible test command builds a dedicated test target and uses the
|
|
project's PostGIS service:
|
|
|
|
```bash
|
|
./scripts/test.sh
|
|
```
|
|
|
|
It creates/updates only the project-scoped `who_need_help_test` database.
|
|
|
|
The complete local CI-equivalent command uses pinned containerized tools and
|
|
an isolated PostgreSQL volume:
|
|
|
|
```bash
|
|
./scripts/quality.sh
|
|
```
|
|
|
|
It checks shell scripts, Dockerfiles, the GitHub Actions workflow, every Compose
|
|
profile, the rendered Helm chart, tracked-source secrets and infrastructure
|
|
misconfigurations, Elixir formatting/compilation/xref/Credo/Sobelow/Dialyzer,
|
|
retired Hex packages, locked npm dependencies, all Phoenix tests, the pinned
|
|
backup-tool, MinIO server/client, external-boundary mock, and production
|
|
release images. Its generated database credentials are random and exist only
|
|
for that run. The exact database volume, networks, temporary source snapshot,
|
|
and one-run images are removed automatically.
|
|
|
|
The same external-service protocol drill used by CI can be run independently:
|
|
|
|
```bash
|
|
./scripts/external-boundaries-run.sh local-boundaries
|
|
```
|
|
|
|
Run the isolated HTTP/WebSocket/authenticated chat/tracking load profile with
|
|
resource, PostgreSQL statement, database-connection, and Ecto pool-wait
|
|
measurements:
|
|
|
|
```bash
|
|
./scripts/load-stack-up.sh
|
|
./scripts/load-run.sh local-load
|
|
./scripts/load-stack-stop.sh
|
|
```
|
|
|
|
The profile has its own generated mode-`0600` environment, Compose project,
|
|
and PostgreSQL volume. It does not use or mutate the ordinary public Compose
|
|
database. Exact inputs and threshold-free evidence are retained below
|
|
`output/performance/local-load/`; see
|
|
[Performance measurement](docs/performance.md) for scope and interpretation.
|
|
|
|
The cursor-pagination database benchmark also creates a one-run Compose
|
|
project, random database credentials, and a separate PostgreSQL volume:
|
|
|
|
```bash
|
|
./scripts/db-scale-benchmark.sh
|
|
```
|
|
|
|
It seeds 50,000 rows in each large benchmark table by default, captures
|
|
PostgreSQL 18 JSON `EXPLAIN (ANALYZE, BUFFERS)` plans before and after the
|
|
cursor-index migration, verifies two consecutive keyset pages for gaps and
|
|
duplicates, and removes its database project and volume. `DB_SCALE_ROWS`
|
|
changes the sample size; it is an experiment input, not a resource minimum.
|
|
Ignored evidence is written below `output/db-scale/`.
|
|
|
|
The browser E2E command creates a uniquely named, isolated Compose project with
|
|
its own PostGIS volume, Mailpit instance, Traefik proxy, two web replicas, and
|
|
two worker replicas:
|
|
|
|
```bash
|
|
./scripts/e2e-run.sh
|
|
```
|
|
|
|
On its first run it generates `.env.e2e` with independent random local secrets
|
|
and mode `0600`. Browser traffic uses the isolated Traefik HTTPS entrypoint;
|
|
Playwright accepts only that one-run proxy's generated certificate. E2E-only
|
|
fixture and failure-injection routes are enabled by a compile-time flag that is
|
|
disabled in the ordinary production image. The suite registers users through
|
|
real Mailpit messages and uses the audited one-time bootstrap command inside
|
|
only the isolated E2E database to create its moderator. It covers
|
|
public/authentication boundaries;
|
|
the urgent medicine flow through matching, realtime chat, handover, and
|
|
double-blind reviews; and the Activity flow through join approval, private
|
|
group chat, message-scoped reporting, blocking, privacy defaults, an unverified
|
|
social link, category moderation, report resolution, and account restriction.
|
|
The same gate checks keyboard skip navigation, WCAG violations and contrast on
|
|
four public pages in both themes, horizontal overflow at three viewport widths,
|
|
an actual locally served raster map tile, LiveView offline/reconnect UI, public
|
|
RU/UK locale persistence, and the selected Ukrainian locale in an authenticated
|
|
LiveView.
|
|
The active failover scenario grants real browser geolocation permission,
|
|
starts consent-based tracking and chat, halts the exact BEAM node serving the
|
|
helper, proves a disconnect and a different runtime boot identity, then checks
|
|
tracking/chat recovery and raw-position deletion in Chromium, Firefox, and
|
|
WebKit.
|
|
An optional list of Playwright spec paths can be passed after the command for a
|
|
focused diagnostic run.
|
|
It retains traces, screenshots, video, and Compose logs under the ignored
|
|
`output/e2e/` directory on failure. The exact E2E project, database volume, and
|
|
networks are removed automatically; the normal `who_need_help` Compose project
|
|
is not recreated.
|
|
|
|
The Android device suite builds dedicated debug and instrumentation APKs,
|
|
boots a fresh emulator in an isolated container without external networking,
|
|
and serves its HTTP fixture only on device loopback. Run the default API 37
|
|
probe or the complete API 30/34/37 matrix:
|
|
|
|
```bash
|
|
./scripts/android-instrumentation-test.sh
|
|
./scripts/android-matrix-test.sh
|
|
```
|
|
|
|
On first run it generates the ignored `.env.android-test` with a randomized
|
|
device-loopback origin and mode `0600`. The suite covers denied and granted
|
|
location permission, same-origin deep links, Activity recreation, foreground
|
|
location upload while the Activity is backgrounded and destroyed, the
|
|
persistent notification Stop action, a disconnected Stop request with visible
|
|
retry state, and externally forced process death for the non-sticky service.
|
|
Results and failure diagnostics are retained by API under ignored
|
|
`output/android-instrumentation/`; the exact emulator container and one-run
|
|
image are removed automatically.
|
|
|
|
The public-staging variant and its instrumentation APK use the explicit HTTPS
|
|
origin from the ignored `.env`. The smoke probe checks rendered WebView DOM on
|
|
the home and Safety routes. The cross-client probe uses run-scoped users and a
|
|
matched medicine request to verify Android login, private chat in both
|
|
directions, foreground location sharing, browser marker appearance and
|
|
removal, and exact database cleanup:
|
|
|
|
```bash
|
|
./scripts/android-staging-build.sh
|
|
./scripts/android-staging-smoke.sh
|
|
./scripts/android-browser-staging-e2e.sh
|
|
```
|
|
|
|
The cross-client script refuses an unexpected database, uses a unique fixture
|
|
prefix and manifest, and compares counts across 19 application tables before
|
|
and after cleanup. It does not delete unrelated records.
|
|
|
|
## First administrator
|
|
|
|
Register and confirm the first account, then explicitly bootstrap it:
|
|
|
|
```bash
|
|
./scripts/bootstrap-admin.sh you@example.com --confirm
|
|
```
|
|
|
|
This succeeds only while no administrator exists and writes an audit event.
|
|
After bootstrap, an administrator can manage roles in `/moderation`; the last
|
|
administrator cannot demote themselves. For kind, append `kind`:
|
|
|
|
```bash
|
|
./scripts/bootstrap-admin.sh you@example.com --confirm kind
|
|
```
|
|
|
|
## Optional shared action limits
|
|
|
|
`RATE_LIMIT_POLICIES_JSON` configures atomic PostgreSQL counters shared by every
|
|
web replica. Its shape is:
|
|
|
|
```json
|
|
{"action_name":{"limit":"POSITIVE_INTEGER","window_seconds":"POSITIVE_INTEGER"}}
|
|
```
|
|
|
|
The strings above describe the required types and are not a runnable policy.
|
|
Keep `{}` until numeric limits have been approved from policy and observed
|
|
traffic. Supported actions are listed in `docs/trust-safety.md`.
|
|
|
|
## Local Kubernetes verification
|
|
|
|
The kind script downloads checksum-verified kubectl, Helm, and kind binaries
|
|
into `.tools/`, creates a project-owned cluster, loads the local image, and
|
|
installs the Helm chart, waits for both Deployments, and verifies cross-node
|
|
PubSub and each BEAM VM's effective port limit:
|
|
|
|
```bash
|
|
./scripts/kind-up.sh
|
|
```
|
|
|
|
Open:
|
|
|
|
- app: <http://localhost:4011>
|
|
- Mailpit: <http://localhost:8028>
|
|
|
|
The script refuses to modify a pre-existing cluster named `who-need-help`
|
|
unless the project ownership marker exists. On first install it creates
|
|
independent random application and PostgreSQL credentials in the
|
|
`who-need-help-local` Kubernetes Secret without writing them to Git. The local
|
|
PostGIS data directory persists inside the kind node. When migrating an older
|
|
project-owned cluster from the former `emptyDir` deployment, the script creates
|
|
and validates a local dump before replacing the database workload, then restores
|
|
that dump. After a successful rollout it also removes the obsolete chart Secret
|
|
and only the local Helm history revisions that stored the former inline
|
|
credential fields.
|
|
|
|
Exercise the verified local rolling-update path without recreating PostGIS or
|
|
the Secret:
|
|
|
|
```bash
|
|
./scripts/kind-rolling-verify.sh local-kind-rollout
|
|
```
|
|
|
|
Compose crash/replacement and Oban retry checks use the separate load project:
|
|
|
|
```bash
|
|
./scripts/load-stack-up.sh
|
|
./scripts/load-resilience-run.sh local-resilience
|
|
```
|
|
|
|
Both scripts retain ignored evidence under `output/resilience/`; their exact
|
|
mutation and cleanup boundaries are documented in the operations runbook.
|
|
|
|
Start and verify the local monitoring profile against every isolated load web
|
|
replica:
|
|
|
|
```bash
|
|
./scripts/observability-run.sh local-observability
|
|
```
|
|
|
|
The command validates Prometheus and Alertmanager configuration, provisions the
|
|
Grafana datasource and dashboard, discovers each current web container as a
|
|
separate target, and exercises a firing/resolved alert by stopping and
|
|
recovering exactly one verified load replica. It prints the loopback-only
|
|
random ports and retains non-secret evidence below `output/observability/`.
|
|
The generated Grafana password remains only in mode-`0600` `.env.load`.
|
|
|
|
Stop only the monitoring services while leaving their local metric volumes and
|
|
the load application running:
|
|
|
|
```bash
|
|
./scripts/observability-stop.sh
|
|
```
|
|
|
|
For an external cluster, provide a real PostgreSQL/PostGIS service and a
|
|
pre-created Secret through required `existingSecret`; the chart never renders
|
|
credentials from tracked values. The Secret must contain `DATABASE_URL`,
|
|
`SECRET_KEY_BASE`, `HANDOVER_SECRET`, `RELEASE_COOKIE`, and `METRICS_TOKEN`, and
|
|
may additionally contain the GitHub OAuth client ID and client secret described
|
|
above. The chart intentionally has no invented CPU/RAM limits or HPA
|
|
thresholds; measure this application in the target environment before setting
|
|
them.
|
|
|
|
## Local Codex category review
|
|
|
|
Export the redacted proposals from a running release:
|
|
|
|
```bash
|
|
docker compose -p who_need_help exec -T web \
|
|
/app/bin/who_need_help eval \
|
|
'WhoNeedHelp.CatalogModeration.export_open_proposals() |> IO.puts()' \
|
|
> proposals.json
|
|
```
|
|
|
|
Then run the advisory review:
|
|
|
|
```bash
|
|
./scripts/codex-review-categories.sh proposals.json recommendations.json
|
|
```
|
|
|
|
The script exits unless `codex login status` is exactly
|
|
`Logged in using ChatGPT`. It uses no OpenAI API key, no usage-based API billing,
|
|
and no fallback provider. Recommendations require a human moderator action.
|
|
|
|
## Design documentation
|
|
|
|
- [Product specification](docs/product-spec.md)
|
|
- [Architecture](docs/architecture.md)
|
|
- [Trust and safety](docs/trust-safety.md)
|
|
- [Operations runbook](docs/operations.md)
|
|
- [Performance measurement](docs/performance.md)
|
|
- [Implementation verification and known limits](docs/verification.md)
|
|
- [Verified dependency baseline](docs/dependency-baseline.md)
|
|
- [PostgreSQL/PostGIS ADR](docs/decisions/0001-postgresql-postgis-over-spacetimedb.md)
|
|
|
|
Exact dependency versions are locked in `mix.lock`,
|
|
`assets/package-lock.json`, the Dockerfile, Compose file, and tool bootstrap
|
|
script.
|