docs(auth): document APP_URL requirement for reverse-proxied deployments (#99) #125

Merged
james merged 1 commit from 99-app-url-docs into main 2026-06-19 12:22:47 +00:00
Owner

Closes #99. Pure docs — no code change.

Why

Self-hosters hit this on first deploy. The container binds on 0.0.0.0:3000; Carol's appOrigin() helper falls back to the request origin when APP_URL is unset, so every OAuth / OIDC redirect_uri comes out pointing at the bind address. The IdP either rejects the request (the registered callback URL doesn't match http://0.0.0.0:3000/...) or completes consent and tries to send the browser to an unreachable URL. The escape hatch was documented in ADR-0015 §2 but didn't appear in any self-hoster-facing doc.

Changes

File Change
docs/oidc-self-hoster-guide.md New top-level "Prerequisite: APP_URL" section before the recipes. Spells out the format (https://<host>, no trailing slash), the failure mode without it, the explicit note that GitHub OAuth has the same requirement (both flows go through the same appOrigin() helper), and a one-line acknowledgement of why auto-derivation from X-Forwarded-* is deliberately not implemented (open-redirect risk if the proxy doesn't strip incoming headers).
docs/oidc-self-hoster-guide.md Troubleshooting New entry for "Sign-in redirects to http://0.0.0.0:3000" → set APP_URL. Cross-links the prerequisite section.
README.md env table APP_URL description shifts from passive ("Set this when…") to required ("Required for any reverse-proxied deployment").
README.md "Postgres + OIDC + reverse proxy" recipe New callout block above the podman command stating APP_URL is mandatory for any reverse-proxied deployment.

Coverage check against the ticket's acceptance criteria

  • Prerequisite paragraph at the top of the OIDC guide.
  • Troubleshooting entry for the 0.0.0.0:3000 redirect symptom.
  • README updated (the env table + the example deployment recipe both reinforce it; CLAUDE.md left alone since it's a contributor doc, not a self-hoster doc).
  • GitHub OAuth same-requirement confirmed in code (both oauth/start and oauth/callback/[provider] call appOrigin(); the GitHub provider is registered in lib/auth/oauth-providers.ts and uses those same routes) and called out in both the prerequisite section and the troubleshooting entry.

Out of scope

Auto-deriving the public URL from X-Forwarded-Proto / X-Forwarded-Host. Per the original issue: it widens the trust surface (the proxy has to be configured to strip incoming X-Forwarded-* from external requests), and a misconfiguration there is an open-redirect vector. If revisited, it'd need its own ticket and an ADR-0015 amendment.

Closes #99. Pure docs — no code change. ## Why Self-hosters hit this on first deploy. The container binds on `0.0.0.0:3000`; Carol's `appOrigin()` helper falls back to the request origin when `APP_URL` is unset, so every OAuth / OIDC `redirect_uri` comes out pointing at the bind address. The IdP either rejects the request (the registered callback URL doesn't match `http://0.0.0.0:3000/...`) or completes consent and tries to send the browser to an unreachable URL. The escape hatch was documented in ADR-0015 §2 but didn't appear in any self-hoster-facing doc. ## Changes | File | Change | |---|---| | `docs/oidc-self-hoster-guide.md` | New top-level "Prerequisite: `APP_URL`" section before the recipes. Spells out the format (`https://<host>`, no trailing slash), the failure mode without it, the explicit note that GitHub OAuth has the **same** requirement (both flows go through the same `appOrigin()` helper), and a one-line acknowledgement of why auto-derivation from `X-Forwarded-*` is deliberately not implemented (open-redirect risk if the proxy doesn't strip incoming headers). | | `docs/oidc-self-hoster-guide.md` Troubleshooting | New entry for "Sign-in redirects to `http://0.0.0.0:3000`" → set `APP_URL`. Cross-links the prerequisite section. | | `README.md` env table | `APP_URL` description shifts from passive ("Set this when…") to required ("**Required for any reverse-proxied deployment**"). | | `README.md` "Postgres + OIDC + reverse proxy" recipe | New callout block above the podman command stating `APP_URL` is mandatory for any reverse-proxied deployment. | ## Coverage check against the ticket's acceptance criteria - [x] Prerequisite paragraph at the top of the OIDC guide. - [x] Troubleshooting entry for the `0.0.0.0:3000` redirect symptom. - [x] README updated (the env table + the example deployment recipe both reinforce it; `CLAUDE.md` left alone since it's a contributor doc, not a self-hoster doc). - [x] GitHub OAuth same-requirement confirmed in code (both `oauth/start` and `oauth/callback/[provider]` call `appOrigin()`; the GitHub provider is registered in `lib/auth/oauth-providers.ts` and uses those same routes) and called out in both the prerequisite section and the troubleshooting entry. ## Out of scope Auto-deriving the public URL from `X-Forwarded-Proto` / `X-Forwarded-Host`. Per the original issue: it widens the trust surface (the proxy has to be configured to strip incoming `X-Forwarded-*` from external requests), and a misconfiguration there is an open-redirect vector. If revisited, it'd need its own ticket and an ADR-0015 amendment.
docs(auth): document APP_URL requirement for reverse-proxied deployments (#99)
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 7s
PR / OSV-Scanner (pull_request) Successful in 41s
PR / Lint (pull_request) Successful in 53s
PR / npm audit (pull_request) Successful in 55s
PR / Typecheck (pull_request) Successful in 56s
PR / Static analysis (pull_request) Successful in 59s
PR / Trivy (image) (pull_request) Successful in 59s
Secrets / gitleaks (pull_request) Successful in 20s
PR / Test (postgres) (pull_request) Successful in 1m26s
PR / Test (sqlite) (pull_request) Successful in 1m33s
PR / Coverage (soft) (pull_request) Successful in 1m35s
PR / Build (pull_request) Successful in 2m13s
8c336e7616
Self-hosters hit this on first deploy: the container binds on
0.0.0.0:3000, Carol's appOrigin() helper reads the request's own
origin when APP_URL isn't set, and every OAuth / OIDC redirect_uri
comes out pointing at the bind address. The IdP either rejects the
request or completes consent and sends the browser to an unreachable
URL. The escape hatch is documented in ADR-0015 §2 but wasn't
surfaced in any user-facing doc.

Three additions:

  - docs/oidc-self-hoster-guide.md picks up a top-level "Prerequisite:
    APP_URL" section before the recipes, spelling out the format
    (`https://<host>`, no trailing slash), the failure mode without
    it, and explicitly noting that the same requirement applies to
    GitHub OAuth (both go through the same appOrigin() helper).
  - Same file's Troubleshooting list picks up an entry for "Sign-in
    redirects to http://0.0.0.0:3000" pointing back at the
    prerequisite section.
  - README's `APP_URL` table entry shifts from passive ("Set this
    when running behind a reverse proxy…") to required ("Required
    for any reverse-proxied deployment"), and the "Postgres + OIDC
    + reverse proxy" recipe gains a callout block above the podman
    command.

Pure docs; no code change. Auto-derivation from X-Forwarded-* is
called out as deliberately not implemented (would need a separate
ticket + ADR amendment per the original issue's "Out of scope" note).

Closes #99.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 84.0% ≥ 50%
Branches 82.6% ≥ 75%
Functions 90.8% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 84.0% ✅ | ≥ 50% | | Branches | 82.6% ✅ | ≥ 75% | | Functions | 90.8% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit fc501f0319 into main 2026-06-19 12:22:47 +00:00
james deleted branch 99-app-url-docs 2026-06-19 12:22:47 +00:00
Sign in to join this conversation.
No description provided.