OpenAI Build Week mutual-aid app for urgent local voluntary help. https://test.whoneedhelp.com
Go to file
2026-08-14 03:53:13 +03:00
.gitea/workflows Separate Android deployment identities 2026-07-23 23:39:56 +03:00
.github Separate Android deployment identities 2026-07-23 23:39:56 +03:00
android Refresh Google Play launch gates 2026-08-14 00:09:10 +03:00
assets Clarify and verify live location consent 2026-08-09 20:52:57 +03:00
config Add confirmation-gated inbound support email 2026-08-13 23:36:29 +03:00
deploy Add confirmation-gated inbound support email 2026-08-13 23:36:29 +03:00
docs Add explicit child safety reporting path 2026-08-14 03:53:13 +03:00
e2e Extend production E2E to staff queues 2026-08-13 07:28:59 +03:00
lib Add explicit child safety reporting path 2026-08-14 03:53:13 +03:00
load/k6 Add persistent million-row discovery scale profile 2026-07-28 18:11:12 +03:00
ops Show transactional email volume by purpose 2026-08-12 20:18:22 +03:00
priv Add explicit child safety reporting path 2026-08-14 03:53:13 +03:00
rel fix: keep clustered nodes on the shared network 2026-07-20 06:50:30 +03:00
scripts Maintain persistent development scale fixtures 2026-08-14 02:09:26 +03:00
test Add explicit child safety reporting path 2026-08-14 03:53:13 +03:00
.dialyzer_ignore.exs fix: harden runtime and reduce request overhead 2026-07-20 06:38:32 +03:00
.dockerignore Add isolated Caddy edge for production and staging 2026-07-21 03:34:37 +03:00
.env.e2e.example Reduce transactional email noise 2026-08-12 16:04:11 +03:00
.env.example Add confirmation-gated inbound support email 2026-08-13 23:36:29 +03:00
.env.load.example Reduce transactional email noise 2026-08-12 16:04:11 +03:00
.formatter.exs feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00
.gitattributes feat: implement Who Need Help MVP 2026-07-18 16:52:16 +03:00
.gitignore feat: scale request discovery by map viewport 2026-07-22 16:37:34 +03:00
AGENTS.md docs: pin Google console browser workflow 2026-07-24 00:50:04 +03:00
compose.backup.yaml feat: add verified encrypted S3 backups 2026-07-19 08:00:12 +03:00
compose.compact.yaml feat: add selectable Compose deployment modes 2026-07-21 01:48:47 +03:00
compose.cpu-replay.yaml Add persistent million-row discovery scale profile 2026-07-28 18:11:12 +03:00
compose.e2e.yaml Fix map recovery and clean transient test resources 2026-07-22 19:45:50 +03:00
compose.edge.yaml Isolate test and production deployments 2026-07-21 17:46:34 +03:00
compose.external-boundaries.yaml feat: add Google authentication and harden sign-in 2026-07-20 23:56:44 +03:00
compose.external-db-socket.yaml Support host PostgreSQL through Unix sockets 2026-07-21 04:35:03 +03:00
compose.external-db.yaml feat: add selectable Compose deployment modes 2026-07-21 01:48:47 +03:00
compose.load.yaml fix: drain replicas and clean isolated load cycles 2026-07-23 05:11:14 +03:00
compose.observability.yaml feat: add verified local observability stack 2026-07-19 07:11:29 +03:00
compose.portability.yaml Isolate clean deployment drill images 2026-07-23 22:56:41 +03:00
compose.production.yaml feat: prepare first production deployment 2026-07-20 21:41:33 +03:00
compose.public-app.yaml Fix public edge interface assignments 2026-07-21 17:08:28 +03:00
compose.quality.yaml fix: harden runtime and reduce request overhead 2026-07-20 06:38:32 +03:00
compose.scale.yaml Add persistent million-row discovery scale profile 2026-07-28 18:11:12 +03:00
compose.upgrade-rehearsal.yaml test: rehearse staging database upgrades 2026-07-19 09:27:16 +03:00
compose.yaml Add confirmation-gated inbound support email 2026-08-13 23:36:29 +03:00
design-qa.md Make request area map draggable 2026-07-22 15:10:44 +03:00
Dockerfile Harden transactional email envelopes 2026-08-12 21:41:01 +03:00
Dockerfile.backup Patch backup DNS dependency 2026-08-13 02:48:57 +03:00
Dockerfile.caddy Prepare Android launch candidate and harden discovery 2026-08-01 00:47:05 +03:00
Dockerfile.minio Prepare Android launch candidate and harden discovery 2026-08-01 00:47:05 +03:00
Dockerfile.postgis fix: harden runtime and reduce request overhead 2026-07-20 06:38:32 +03:00
Dockerfile.socket-proxy fix: harden runtime and reduce request overhead 2026-07-20 06:38:32 +03:00
Dockerfile.traefik Prepare Android launch candidate and harden discovery 2026-08-01 00:47:05 +03:00
LICENSE Add MIT license 2026-07-22 03:37:49 +03:00
mix.exs Update Postgrex for CVE-2026-66838 2026-08-08 13:04:23 +03:00
mix.lock Harden launch verification and update LiveView 2026-08-10 16:45:10 +03:00
README.md Add confirmation-gated inbound support email 2026-08-13 23:36:29 +03:00

Who Need Help

Who Need Help is a working mutual-aid MVP for urgent, local, voluntary help. The first priority is asking whether a nearby volunteer is willing to purchase, pick up, and deliver lawful medicine. Accepting never obliges a helper to spend money; purchase and reimbursement arrangements remain directly between the matched people. 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, medical, pharmacy, or payment service, does not prescribe or sell medicine, and does not guarantee reimbursement. A helper may publish an optional external thank-you link; money goes directly between users outside the platform.

OpenAI Build Week 2026

Who Need Help was created during the OpenAI Build Week submission period. The entrant supplied the real-world problem, product priorities, safety decisions, and deployment constraints. Codex running GPT-5.6 (gpt-5.6-sol) was used to turn those decisions into the Phoenix application, tests, deployment tooling, documentation, and browser verification. Important product choices—voluntary help, medicine-first scope, consent-based tracking, two-party handover, and no platform payments—remain human decisions rather than model-generated policy.

The deployed application does not call the OpenAI API and contains no OpenAI API key. Optional category-review assistance runs only through the operator's local Codex CLI authenticated with their ChatGPT subscription.

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, with explicit start, arrival, handover, both-party confirmation, requester cancellation, helper withdrawal/reopening, replacement-helper, and expiry paths.
  • PostgreSQL/PostGIS locations, viewport-scoped request discovery, server-side map clustering, MapLibre map, private matched chat, Phoenix PubSub/Presence, and optional consent-driven live location sharing. The browser loads only the chosen map area; exact points, privacy areas, and coordinate-free requests retain their distinct disclosure rules.
  • 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.
  • A private notification inbox, category/radius/urgency/availability-based nearby-help subscriptions, quiet hours, per-channel preferences, browser Web Push registrations, and Android FCM device registrations. Nearby matches use the inbox and optional real-time push; immediate nearby email is disabled and a batched email digest is not implemented yet. Remote payloads contain navigation metadata and generic text, never chat bodies or exact coordinates; Oban retries transient delivery failures and disables rejected device registrations.
  • Bidirectional discovery blocks, scoped reports, account/request/category moderation, abuse-signal review, and audited moderator access to only the conversation linked by a report. A unified staff workspace uses combinable support, moderator, legal, analyst, and administrator roles; administrators manage users and staff access while the last active administrator is protected.
  • 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, verification-gated support alerts, and audited staff queues. Support sends one initial response email and later public-status changes through the dedicated Oban mail queue; ordinary conversation messages stay in the private inbox and optional push. Content-removal confirmation, receipt, and public decision updates use the same bounded queue, while internal assignment-only changes do not email the submitter. Authenticated users can download an allow-listed JSON data export that omits password/session/push credentials and counterpart message bodies. A moderator-only deletion preflight reports active workflows without performing an unapproved destructive action.
  • 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, consent-based FCM registration/deep links, 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 debug and stable-signed staging APKs plus a production-signed APK and Play AAB. The web app publishes environment-specific verified Android App Links metadata and the build verifies package/certificate agreement. Play registration, Play App Signing identity, store review, and physical device FCM delivery are still external release steps.
  • 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, Android 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 an independent server-level Caddy edge. Production and test 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 for the verified order of operations. Redis is not a project dependency.

Prerequisite: Docker with the Compose plugin.

./scripts/deploy-up.sh .env

Open:

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. The Traefik image is reproducibly built from the checksum-pinned upstream v3.7.10 source with grpc-go 1.82.1. The Caddy edge, backup image, and optional MinIO images build upstream Caddy v2.11.4 (with grpc-go 1.82.1 and golang.org/x/text 0.40.0), restic v0.19.1, MinIO, and mc with the same dependency pin. Those upstream dependency graphs still contain older grpc-go versions affected by GHSA-hrxh-6v49-42gf.

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

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:

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:

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:

./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:

./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:

./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 output/runtime/load.env, 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:

./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. Use the public launch checklist to keep automated evidence separate from provider, staffing, and jurisdiction-specific approvals.

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:

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

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 and requires the relay's SMTP_* credentials. 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 alerts for authenticated or email-verified support cases and make replies return to the support team; unverified public support stays outside the operator queue. The database queues continue to work when it is empty. See the support and content-removal runbook. That address is not proof of inbound delivery. Optional Brevo inbound parsing uses the paired SUPPORT_INBOUND_RECIPIENT and SUPPORT_INBOUND_WEBHOOK_TOKEN settings; it deduplicates provider messages and still requires the sender to confirm the mailbox before staff can see the case. Then validate the file structure and the production Compose render:

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

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

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

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/test roles and empty databases, preloads citext and PostGIS, verifies both logins, and writes mode-0600 initializer fragments without printing their passwords:

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
unset PRODUCTION_DATABASE_MODE PRODUCTION_DATABASE_URL \
  PRODUCTION_DATABASE_SOCKET_DIR

Use database-test.env for the isolated test 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:

./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:

./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:

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.

For Android Credential Manager, set GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS to the comma-separated OAuth client IDs registered for the Android package/signing certificates in that same environment. Phoenix still requires the Web client ID as the token audience; the Android client ID is accepted only as the verified azp (authorized party) claim.

The flow requests openid email profile, verifies the provider email claim, uses state, nonce, and PKCE, and discards provider tokens. A new Google identity continues to a safe registration-completion page and creates a confirmed local account only after the user accepts the 18+ safety terms. A returning linked identity signs in directly. 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.

The Android app does not open Google OAuth inside the WebView. Its visible Google button uses Android Credential Manager, asks this same server for a session-bound one-time nonce, and sends the resulting ID token directly back to the server. The server verifies the signature, algorithm, issuer, Web audience, allowlisted Android authorized party, expiration, issued-at time, verified email, and nonce before it reuses the ordinary login, registration, or account-linking rules. GOOGLE_OAUTH_CLIENT_SECRET remains server-only; neither it nor the ID token is exposed to WebView JavaScript or compiled into the APK.

Optional verified GitHub linking

Create a GitHub OAuth App with this exact callback URL for the active public origin:

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:

./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:

./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:

./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:

./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. The lifecycle wrapper is preferred because it removes only its unique containers, networks, volumes, environment, and image tags on success, failure, or interruption:

./scripts/load-cycle.sh local-load load

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 for scope and interpretation.

A separate production probe is limited to a compiled allowlist of public GET pages and Phoenix heartbeat frames. First inspect a read-only plan with every experiment input supplied explicitly:

WNH_PRODUCTION_LOAD_HTTP_VUS=<chosen-count> \
WNH_PRODUCTION_LOAD_WS_VUS=<chosen-count> \
WNH_PRODUCTION_LOAD_DURATION=<chosen-duration> \
WNH_PRODUCTION_LOAD_HTTP_THINK_SECONDS=<chosen-seconds> \
WNH_PRODUCTION_LOAD_WS_HOLD_MS=<chosen-milliseconds> \
WNH_PRODUCTION_LOAD_WS_CONNECT_TIMEOUT_MS=<chosen-milliseconds> \
  ./scripts/production-readonly-load.sh plan

It does not start load in plan mode. The separately approved run mode requires the exact confirmation printed by that plan and still cannot test authenticated writes or establish a production capacity limit.

The cursor-pagination database benchmark also creates a one-run Compose project, random database credentials, and a separate PostgreSQL volume:

./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:

./scripts/e2e-run.sh

On its first run it generates output/runtime/e2e.env 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; 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; and the support/legal flow through authenticated support, operator reply and resolution, private requester status, general content removal, dedicated TAKE IT DOWN intake, legal-queue processing, and decision email delivery through the isolated Mailpit server. 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:

./scripts/android-instrumentation-test.sh
./scripts/android-matrix-test.sh

On first run it generates ignored output/runtime/android-test.env 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 development and staging variants and their instrumentation APKs use the explicit HTTPS origin from each checkout's ignored .env. Development uses org.whoneedhelp.mobile.development; the independent test checkout uses org.whoneedhelp.mobile.staging. 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:

./scripts/android-development-build.sh
./scripts/android-development-smoke.sh
./scripts/android-browser-development-e2e.sh

# Run these only from the independent test checkout.
./scripts/android-staging-build.sh
./scripts/android-staging-smoke.sh
./scripts/android-browser-staging-e2e.sh
./scripts/android-browser-staging-e2e.sh --remote \
  SSH_HOST:/absolute/path/to/test/checkout

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. Remote mode accepts only a container-backed deployment with DEPLOYMENT_ENV=test and a Compose project ending in _test; it keeps the Android emulator and browser local, reaches the remote Mailpit API through a run-scoped loopback SSH tunnel, and executes fixture preparation, verification, and exact cleanup next to the remote test database. It does not delete unrelated records.

The exact production foreground-service recording flow has a separate run-scoped operator fixture. It creates one temporary browser account and one matched request for an already confirmed physical-device helper, retains its exact IDs in ignored mode-0600 state, verifies the active and stopped location states, and performs exact cleanup:

# Run only from /srv/who_need_help-production after reading the Play declaration.
./scripts/production-play-physical-fixture.sh \
  plan HELPER_EMAIL --check-only whoneedhelp.com .env

The mutating prepare, verify-active, verify-stopped, and cleanup commands are documented in android/play-store/location-and-fgs-declaration.md. The wrapper refuses the independent hackathon test checkout and any unexpected production root, origin, Compose project, image, health state, or database.

First administrator

Register and confirm the first account, then explicitly bootstrap it:

./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 multiple staff roles in /admin/users; the last active administrator cannot remove their own admin access or be restricted. The complete role matrix and operator workflow are in docs/staff-operations.md. For kind, append kind:

./scripts/bootstrap-admin.sh you@example.com --confirm kind

Shared action limits

RATE_LIMIT_POLICIES_JSON configures atomic PostgreSQL counters shared by every web replica. Its shape is:

{"action_name":{"limit":"POSITIVE_INTEGER","window_seconds":"POSITIVE_INTEGER"}}

The strings above describe the required types and are not a runnable policy. The shipped pilot policy covers public authentication and anonymous support/content-removal intake. Account/email ceilings are lower than IP ceilings so a shared network is not treated as one person. The complete effective JSON is kept in .env; an absent value uses the compiled pilot default, while an explicit {} disables all counters for isolated load/E2E runs. These values are an initial product policy, not universal security or capacity thresholds. Supported actions are listed in docs/trust-safety.md.

Measure the limiter on a disposable PostgreSQL database and a one-CPU application container by supplying the workload explicitly:

./scripts/rate-limit-benchmark.sh ATTEMPTS CONCURRENCY DISTRIBUTED_SCOPES

The script measures a single contended bucket and a distributed-scope profile, writes JSON under ignored output/rate-limit/, and removes only its uniquely named Compose project, volume, containers, and images on success, failure, or interrupt. It does not infer a production limit from the result.

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:

./scripts/kind-up.sh

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

./scripts/kind-stop.sh

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

Open:

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.

When the ignored mode-0600 .env exists, every kind-up.sh run also reconciles a fixed allowlist of development provider settings into that same Secret: Google/GitHub sign-in, browser push, FCM delivery, Android App Links, sender identity, and support routing. It never prints their values and does not replace the independently generated database or application secrets. Empty allowlisted values remove stale provider settings so .env remains the single development source of truth.

Exercise the verified local rolling-update path without recreating PostGIS or the Secret:

./scripts/kind-rolling-verify.sh local-kind-rollout

Compose crash/replacement and Oban retry checks use the separate load project:

./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:

./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 output/runtime/load.env.

Stop only the monitoring services while leaving their local metric volumes and the load application running:

./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 to smtp; keep SMTP credentials in that Secret. 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:

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:

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

Exact dependency versions are locked in mix.lock, assets/package-lock.json, the Dockerfile, Compose file, and tool bootstrap script.