# 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 passwordless email magic links,
  optional passwords, optional Google OpenID Connect registration/sign-in, 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.
- Separate public support and content-removal intake, including moderation
  appeals, account deletion/data requests, a URL-only TAKE IT DOWN form,
  verified-contact status links, operator alerts, and audited staff queues.
- 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`, combined `app`, 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

The public single-server path uses the compact application topology plus a
separately managed Caddy edge. Production and staging can run as isolated
Compose projects with distinct PostGIS volumes and secrets while sharing only a
Docker network used for HTTPS reverse proxying. See the
[operations runbook](docs/operations.md#public-https-edge-and-isolated-staging)
for the verified order of operations. Redis is not a project dependency.

Prerequisite: Docker with the Compose plugin.

```bash
./scripts/deploy-up.sh .env
```

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.

Four deployment settings in the ignored environment select the runtime without
editing Compose files:

```dotenv
DEPLOYMENT_TARGET=compose       # compose or kubernetes
APP_TOPOLOGY=split             # split or compact
DATABASE_MODE=container        # container or external
DATABASE_URL=ecto://...        # the selected database
```

`split` keeps independently scalable web and worker containers and uses
Traefik; `WEB_REPLICAS` and `WORKER_REPLICAS` default to 2. `compact` starts one
Phoenix+Oban container and exposes it directly to the host reverse proxy. It is
the lower-overhead first-server mode. `container` starts the project-owned
PostGIS service. `external` excludes that service from the active Compose
model, checks PostgreSQL/PostGIS connectivity, applies migrations, and only
then starts the application. There is no Redis service: Oban, rate limits, and
cross-instance coordination use PostgreSQL.

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 GitHub OAuth, Google OIDC, 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
and a separate metrics bearer token 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.

For a first Compose deployment, generate an ignored environment on the target
Docker host. The default is `APP_TOPOLOGY=compact` with a project-owned
PostGIS container. The command derives that host's Docker socket group,
generates independent database, Phoenix, handover, cluster, and metrics
secrets without printing them, writes mode `0600`, and refuses to replace an
existing file:

```bash
./scripts/init-production-env.sh whoneedhelp.com .env.production
```

Configure the verified reverse-proxy source IP/CIDR and transactional email
provider in that file. `EMAIL_DELIVERY_PROVIDER=smtp` uses the standard Swoosh
SMTP adapter. `EMAIL_DELIVERY_PROVIDER=unisender_go` uses UniSender Go's HTTPS
Web API and requires `UNISENDER_GO_API_KEY`; this mode does not need outbound
SMTP ports. Email registration and magic-link login are unusable for real
recipients until the selected provider and its accepted sender are configured. Set
the optional `SUPPORT_INBOX_ADDRESS` to a monitored mailbox to receive
metadata-only new-case alerts and make replies return to the support team; the
database queues continue to work when it is empty. See
[the support and content-removal runbook](docs/support-and-content-removal.md).
Then validate the file structure and the production Compose render:

```bash
./scripts/validate-production-env.sh .env.production whoneedhelp.com
./scripts/deploy-up.sh .env.production
```

To generate a split deployment against an already provisioned PostgreSQL 18 +
PostGIS database, supply the mode and URL to the initializer:

```bash
PRODUCTION_APP_TOPOLOGY=split \
PRODUCTION_DATABASE_MODE=external \
PRODUCTION_DATABASE_URL='ecto://USER:PASSWORD@DB_HOST/DB_NAME?ssl=true' \
  ./scripts/init-production-env.sh whoneedhelp.com .env.production
```

Use `ssl=true` when the database provider requires TLS; the database check
reports whether the observed connection uses TLS without printing the URL or
credentials. Provider-specific CA/network requirements still have to be
configured from that provider's verified documentation.

For PostgreSQL installed on the same Linux host, keep its TCP listener private
and connect through its Unix socket. The root-only bootstrap refuses existing
project roles/databases, backs up `pg_hba.conf`, adds two exact SCRAM rules,
creates independent production/staging roles and empty databases, preloads
`citext` and PostGIS, verifies both logins, and writes mode-`0600` initializer
fragments without printing their passwords:

```bash
sudo ./scripts/provision-host-postgres.sh "$USER"
set -a
. "$HOME/.config/who_need_help/database-production.env"
set +a
./scripts/init-production-env.sh whoneedhelp.com .env.production
unset PRODUCTION_DATABASE_MODE PRODUCTION_DATABASE_URL \
  PRODUCTION_DATABASE_SOCKET_DIR
```

Use `database-staging.env` for the isolated staging environment. Compose mounts
only the configured socket directory read-only; Ecto migrations remain the
source of application schema. Inspect the exact host PostgreSQL state and the
script's documented impact before the sudo invocation.

`compose.production.yaml` leaves local Mailpit stopped. Validation deliberately
fails while the relay still points to Mailpit or a template marker remains.
It does not claim to test DNS, certificates, actual mail delivery, the
deployment's observed proxy source address, or capacity; verify those on the
target host before opening registration.

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.

To rotate only the local PostgreSQL role and matching `DATABASE_URL`, without
changing Phoenix sessions or handover codes, use:

```bash
./scripts/rotate-local-secrets.sh --database-only
docker compose up -d --wait
```

### Optional Google registration and sign-in

Create a Google OAuth 2.0 Web application only for an owned HTTPS origin. Add
this exact authorized redirect URI in Google Cloud:

```text
https://YOUR_PHX_HOST/auth/google/callback
```

Set both `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET` in the
ignored `.env` or deployment Secret, then recreate the application containers.
If both values are empty, the Google buttons remain visible but disabled with
an explanation. A partial pair is rejected at startup and by the production
environment validator.

The flow requests `openid email profile`, verifies the provider email claim,
uses state, nonce, and PKCE, and discards provider tokens. Google registration
creates a confirmed local account only after the user accepts the 18+ safety
terms. Google login works only for an identity already linked to that local
account. An existing local account is never merged merely because Google
returns the same email; sign in by email or password and connect Google from
the sudo-protected account settings page instead. Leave
`GOOGLE_OAUTH_BASE_URL` and the Google HTTP timeout variables empty outside the
isolated protocol drill.

### 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.

For a final or reviewable local run, use the recorded wrapper from a clean
Git worktree:

```bash
./scripts/quality-record.sh final-audit
```

It runs the same isolated gate and writes `quality.log` plus `summary.txt`
under `output/regression/final-audit/`. The run directory is private to the
current user, the files are mode `0600`, and the summary records the exact Git
commit/tree, tool versions, exit codes, test result, and log SHA-256. The
generated `output/` tree remains excluded from Git because audit logs can
contain environment-specific diagnostic details.

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 minimum/current API 24/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
```

Pause the local cluster after Kubernetes verification without deleting its
container, PostGIS volume, Secret, or other cluster state:

```bash
./scripts/kind-stop.sh
```

`./scripts/kind-up.sh` resumes a stopped project-owned control plane before
loading images and reconciling the chart.

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 OAuth credentials described above. Set
`app.emailDeliveryProvider`; UniSender Go API mode also requires
`UNISENDER_GO_API_KEY` in that Secret, while SMTP credentials remain Secret
values in SMTP mode. 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.
