333 lines
14 KiB
Markdown
333 lines
14 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
|
|
```
|
|
|
|
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"
|
|
```
|
|
|
|
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.
|
|
|
|
## 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, and the
|
|
production release image. 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 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`. 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.
|
|
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 a dedicated debug and instrumentation APK,
|
|
boots an API 37 emulator in an isolated container without external networking,
|
|
and serves its HTTP fixture only on device loopback:
|
|
|
|
```bash
|
|
./scripts/android-instrumentation-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, the persistent notification Stop action, and a disconnected
|
|
Stop request with visible retry state. Results and failure diagnostics are
|
|
retained under ignored `output/android-instrumentation/`; the exact emulator
|
|
container and one-run image are removed automatically.
|
|
|
|
## 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:
|
|
|
|
```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.
|
|
|
|
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 both GitHub OAuth variables 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.
|