diff --git a/apps/docs/deployment/configuration.mdx b/apps/docs/deployment/configuration.mdx index 9f31c559..d90c5246 100644 --- a/apps/docs/deployment/configuration.mdx +++ b/apps/docs/deployment/configuration.mdx @@ -7,6 +7,42 @@ description: "Reference for Harly environment variables: required secrets, stora `https://hiring.example.com`. Every secret must be independent — do not reuse values between variables. +## `HARLY_URL` validation + +Harly derives every public URL from `HARLY_URL` rather than the incoming +request origin. That includes upload presign URLs, authenticated file URLs, +OAuth callbacks, redirects, page metadata, outbound webhook payloads, +candidate notifications, and integration callbacks. A misconfigured value +therefore leaks into links your candidates and integrations receive, so Harly +validates it at startup and refuses to boot when it is unsafe. + +In production (`NODE_ENV=production`), `HARLY_URL` must: + +- Be an absolute URL with an `https://` scheme. Plain `http://` is rejected. +- Resolve to a routable public hostname. Harly rejects `localhost`, any + `*.localhost` subdomain, the IPv4 loopback range `127.0.0.0/8`, the IPv6 + loopback `::1` (and `[::1]`), and the unspecified bind addresses `0.0.0.0`, + `::`, and `[::]`. +- Contain no userinfo, query string, or fragment. Only scheme, host, and + optional port are accepted. + +Outside production, `http://` is allowed so local development against +`http://localhost:3000` continues to work. + +If validation fails, Harly throws at startup with a message such as +`HARLY_URL must use HTTPS in production.` or `HARLY_URL must use a reachable +public hostname, not a local or bind address.`. Fix the value in `.env` (or +your platform's secret manager) and restart the app. + + + Behind a reverse proxy, set `HARLY_URL` to the public hostname candidates + and integrations see — never the container's internal bind address. Values + like `http://0.0.0.0:3000` or `http://127.0.0.1:3000` are rejected in + production, and even when accepted they would produce broken links in + emails, webhooks, and OAuth callbacks. See + [proxy modes](/self-hosting/proxy-modes) for reverse-proxy setup. + + ## Required runtime values ```dotenv diff --git a/apps/docs/self-hosting/proxy-modes.mdx b/apps/docs/self-hosting/proxy-modes.mdx index b2a8c445..83376b74 100644 --- a/apps/docs/self-hosting/proxy-modes.mdx +++ b/apps/docs/self-hosting/proxy-modes.mdx @@ -50,6 +50,16 @@ Point your existing certificate management (Certbot, an ACME client, or a load b The Nginx block above is a standard reverse-proxy pattern, not a file shipped by Harly. Adapt it to your existing proxy configuration. +### Set `HARLY_URL` to the public hostname + +Harly builds upload presign URLs, authenticated file URLs, OAuth callbacks, redirects, page metadata, outbound webhooks, and integration callbacks from `HARLY_URL`, not from the incoming request. Set it to the public origin that candidates and integrations reach: + +```dotenv +HARLY_URL=https://careers.example.com +``` + +Never point `HARLY_URL` at the container's internal bind address. In production, Harly rejects `HARLY_URL` values that use `http://` or resolve to `localhost`, the IPv4 loopback range `127.0.0.0/8`, `::1`, `0.0.0.0`, or `::`, and refuses to start. See [`HARLY_URL` validation](/deployment/configuration#harly-url-validation) for the full list of rules. + ### Trust your reverse proxy for client IPs By default, Harly reads the client IP from the rightmost hop in `X-Forwarded-For`. In `external` mode that hop is your reverse proxy, not the end user. Workspace IP allowlists, per-IP rate limits, and audit log `ipAddress` fields will all see the proxy's address instead of the real client until you tell Harly which peer to trust.