# reCAPTCHA on Registration and Password Reset > Goal: verify the user is not a bot **before** sending any email — registration > verification code or password reset code. The captcha gate sits between form > submission and the email-sending call, so a failed check never triggers an email. --- ## 1. reCAPTCHA v2 (checkbox) reCAPTCHA v2 with the "I'm not a robot" checkbox. Reasons: - Binary pass/fail — no score threshold to tune or monitor. - The user is already filling a form; one checkbox is negligible friction. - Works in the web SPA. It works in the Electron client too, but not for the reason "both run Chromium" — reCAPTCHA validates the *domain*, not the rendering engine, and the desktop client's is not the hub's. See §6. - v3 (invisible, score-based) is an option later if the checkbox proves annoying; the server-side verification call is identical, only the client widget differs. **Google Console setup:** create a reCAPTCHA v2 key pair at `https://www.google.com/recaptcha/admin`. Register the hub's domain(s) — `meshbay.org` and `localhost` for development. This produces a **site key** (public, embedded in HTML) and a **secret key** (server-only, in `hub.toml`). If the desktop client is in use, also turn *off* "Verify the origin of reCAPTCHA solutions" on that key and set `allowed_hosts` — §6 says why, and what is given up. --- ## 2. Configuration ### `hub.toml` ```toml [captcha] site_key = "6Le..." # public — served to the frontend secret_key = "6Le..." # private — never leaves the server ``` When the `[captcha]` section is absent or both keys are empty, the captcha is **disabled** — the registration endpoint accepts requests without a token. This keeps development, tests and self-hosted instances that do not need it frictionless. ### `config.py` — new dataclass ```python @dataclass class CaptchaConfig: site_key: str = "" secret_key: str = "" @property def enabled(self) -> bool: return bool(self.site_key and self.secret_key) ``` Add `captcha: CaptchaConfig` to `HubConfig` (default: disabled). Parse the `[captcha]` section in `load_config` on the same pattern as `[jwt]`: ```python if cap := raw.get("captcha", {}): cfg.captcha.site_key = cap.get("site_key", cfg.captcha.site_key) cfg.captcha.secret_key = cap.get("secret_key", cfg.captcha.secret_key) ``` Environment variable overrides: `MESHBAY_CAPTCHA_SITE_KEY`, `MESHBAY_CAPTCHA_SECRET_KEY`. --- ## 3. Serving the site key to the frontend The site key is public and the SPA needs it before the user reaches the registration form. Two options: **Option A — extend `/v1/hub/info`** (recommended). Add `captcha_site_key` to the response (empty string when disabled). The SPA already calls this endpoint at startup for `allow_public_groups`; no new request. The endpoint is unauthenticated, which is correct — the site key is public by design. ```python # hub.py — hub_info() return { ... "captcha_site_key": _cfg.captcha.site_key if _cfg and _cfg.captcha.enabled else "", } ``` **Option B — inject in the HTML shell.** Add a `