Merge pull request #330 from trails-cool/document-https-dev-special-cases

Document why HTTPS=1 dev exists, and the one case that still needs it
This commit is contained in:
Ullrich Schäfer 2026-04-26 12:30:12 +02:00 committed by GitHub
commit ba3a619160
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 2 deletions

View file

@ -68,6 +68,40 @@ pnpm db:push # Push Drizzle schema to local PostgreSQL
pnpm db:studio # Open Drizzle Studio (DB browser)
```
### Local HTTPS dev (rare — most contributors never need this)
The default dev loop runs the journal on plain HTTP at
`http://localhost:3000`. WebAuthn passkeys, magic links, sessions, the
Terms gate, SSE — everything works over HTTP because the WebAuthn spec
treats `localhost` as a secure context regardless of scheme. CI's e2e
suite runs over plain HTTP too.
There is exactly one feature that requires local HTTPS: **Wahoo OAuth**.
Wahoo (and most OAuth providers) reject `http://` redirect URIs, so the
`/api/sync/connect/wahoo` callback flow can only complete against an
HTTPS dev server. To run that flow:
```bash
HTTPS=1 ORIGIN=https://localhost:3000 pnpm --filter @trails-cool/journal dev
```
`HTTPS=1` enables the `@vitejs/plugin-basic-ssl` cert + the ALPN
HTTP/1.1 workaround in `apps/journal/vite.config.ts`. `ORIGIN` makes the
WebAuthn server expect the HTTPS origin (set this together with HTTPS=1
or you'll get origin-mismatch errors). Use `pnpm --filter` (not
`pnpm dev`) because turbo doesn't pass `HTTPS` through unless added to
its `globalPassThroughEnv``pnpm --filter` bypasses turbo entirely.
**Don't set `ORIGIN=https://localhost:3000` in your `apps/journal/.env`
unless you intend to always run with `HTTPS=1`.** Mismatched values
break the e2e suite and generic dev. See
`apps/journal/.env.example` for what each var means.
If you find yourself wanting `HTTPS=1` for any reason other than Wahoo
testing, write it down here so the assumption stays auditable —
"everything but Wahoo works over HTTP locally" is what keeps CI and
local config symmetric.
## Testing Strategy
- **Unit tests** (Vitest + jsdom): For packages, components, utilities, and app logic.

55
apps/journal/.env.example Normal file
View file

@ -0,0 +1,55 @@
# Journal local-dev environment.
# Copy to `.env` (gitignored) and edit. Most contributors don't need
# anything in this file — the journal app boots on plain HTTP at
# http://localhost:3000 with sensible defaults.
# ────────────────────────────────────────────────────────────────────
# DO NOT SET unless you know you need it
# ────────────────────────────────────────────────────────────────────
#
# `ORIGIN` is what the WebAuthn ceremony uses as `expectedOrigin`.
# When unset it defaults to `http://localhost:3000`, which is what
# Playwright sends and what the browser sees in plain-HTTP dev.
#
# The ONLY reason to set this is if you're running the dev server
# over HTTPS (see HTTPS=1 below) and want passkey registration to
# succeed against the HTTPS origin. If you set it to `https://...`
# without also running the server over HTTPS, you'll get
# "Unexpected registration response origin" errors in dev and the
# local e2e suite (which always hits HTTP) will fail registration.
#
# ORIGIN=https://localhost:3000
# Flagship marker. Renders the project marketing block on the
# anonymous home and gates a few "this is the canonical instance"
# behaviors. Self-hosted instances leave this unset; flagship CI
# sets it to `true`. Either is fine for local dev — set it if you
# want to see the full marketing layout, leave it unset if you
# want the self-host home view.
# IS_FLAGSHIP=true
# ────────────────────────────────────────────────────────────────────
# Wahoo OAuth (only needed when actively testing the Wahoo import)
# ────────────────────────────────────────────────────────────────────
#
# Wahoo's OAuth flow is the one feature that genuinely needs HTTPS
# locally: the provider rejects `http://` redirect URIs. To exercise
# the connect/disconnect/import flow end-to-end, you need:
#
# 1. Local HTTPS dev server: run `HTTPS=1 pnpm --filter
# @trails-cool/journal dev` (the basic-ssl plugin in
# vite.config.ts wires up the cert; see the comment there for
# the ALPN workaround).
# 2. ORIGIN=https://localhost:3000 (uncomment above).
# 3. Wahoo client credentials below, and a redirect URI of
# `https://localhost:3000/api/sync/callback/wahoo` registered
# in your Wahoo developer dashboard.
#
# WAHOO_CLIENT_ID=
# WAHOO_CLIENT_SECRET=
# WAHOO_WEBHOOK_TOKEN=
# Integration test secret (only needed if running the integration
# test suite that drives the API directly). Generate with
# `openssl rand -hex 32`.
# INTEGRATION_SECRET=

View file

@ -11,6 +11,26 @@ export default defineConfig({
plugins: [
tailwindcss(),
reactRouter(),
// Local HTTPS dev is OPT-IN via `HTTPS=1`. Plain HTTP is the default
// and the right choice for ~everything: WebAuthn (passkeys) treats
// `localhost` as a secure context regardless of scheme, magic links
// work over HTTP, sessions / Terms gate / SSE all work over HTTP.
// CI doesn't set HTTPS=1 either — the e2e suite runs against the
// production `react-router-serve` build over plain HTTP and passes
// cleanly. So most contributors should never touch this flag.
//
// The one case where you DO need HTTPS=1 locally:
// - Wahoo OAuth callback testing — Wahoo (and most OAuth
// providers) reject `http://` redirect URIs, so the
// /api/sync/connect/wahoo flow can only complete against an
// HTTPS dev server. See docs/tooling.md for the full
// command (`HTTPS=1 ORIGIN=https://localhost:3000 pnpm
// --filter @trails-cool/journal dev`) and gotchas.
//
// If you find yourself wanting HTTPS=1 for any other reason, write
// it down in docs/tooling.md — the assumption that "everything but
// Wahoo works over HTTP locally" is what keeps the CI/local
// configurations symmetric.
...(process.env.HTTPS === "1" ? [import("@vitejs/plugin-basic-ssl").then((m) => m.default())] : []),
sentryVitePlugin({
org: "trails-qq",
@ -36,8 +56,8 @@ export default defineConfig({
// `singleFetchAction` compares `Origin` against the `Host` header,
// which HTTP/2 replaces with `:authority` and Node doesn't
// synthesize back. Restricting ALPN to `http/1.1` keeps the Host
// header intact and lets fetcher form submissions through.
// Plain HTTP dev (the default) is unaffected.
// header intact and lets fetcher form submissions through. Only
// matters when `HTTPS=1` is set — see the plugins comment above.
https: process.env.HTTPS === "1" ? { ALPNProtocols: ["http/1.1"] } : undefined,
},
});