Garmin Connect as the third connected-services provider (spec:
garmin-import). The interesting parts:
- Push-first ingestion: Garmin has no list endpoint. The webhook
normalizes ping (callbackURL) and push (inline) notification batches
into events; the slow work (authorized FIT download, FIT→GPX via the
shared converter, activity creation) runs in a garmin-import-activity
pg-boss job so the webhook answers fast. Callback URLs are validated
against Garmin's API host before any fetch (SSRF guard).
- History via backfill requests: /sync/import/garmin is a date-range
requester with honest async progress (no pick list — the concept
doesn't exist in a push model). Ranges chunk to Garmin's 90-day cap;
overlaps are free via sync_imports dedupe. Requests persist in
import_batches via two new nullable columns (range_start/range_end).
- OAuth2 + PKCE on the existing oauth credential kind. Design
correction from apply: the verifier rides a short-lived httpOnly
cookie scoped to the callback path — the state param is visible in
redirect URLs and must never carry it. Manifests opt in via pkce:true.
- Deregistration notifications flip the connection to 'revoked'
(row kept for audit, imports retained, re-connect prompt shown).
- Framework evolutions, all additive: parseWebhook returns
WebhookEvent[] (Garmin batches; Wahoo adapted), manifest gains
configured()/importUrl/pkce, importActivity accepts summary stats
for FIT-less imports, manager gains markRevoked.
- Env-gated: no GARMIN_CLIENT_ID → provider hidden on
/settings/connections. Privacy manifest entry (DE+EN). i18n en+de.
Rollout (§6) stays gated on the Garmin Developer Program application
(submitted 2026-06-07). Fixtures are doc-shaped; the staging soak
swaps in recorded payloads if shapes differ.
Gate: typecheck ✓ lint ✓ unit+integration ✓ e2e 70/72 + both known
flakes green isolated ✓ openspec validate ✓
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Most contributors don't need HTTPS=1 locally — plain HTTP is the
default and the right choice for everything except Wahoo OAuth
callback testing. WebAuthn (passkeys), magic links, sessions, the
Terms gate, SSE all work over HTTP because the WebAuthn spec treats
localhost as a secure context regardless of scheme. CI proves the
point: the e2e suite runs over plain HTTP and passes cleanly.
The original HTTPS=1 plumbing landed in 20b91ef (2026-04-05) bundled
into a Wahoo import fix, with no inline rationale. Once you've
forgotten the reason it tends to leak into the default workflow,
which then breaks the local e2e suite (Playwright always uses HTTP
baseURL; an https ORIGIN env mismatches what it sends) and creates
unnecessary divergence from CI.
Documenting the single legitimate use case so the next contributor
(or future-me) doesn't have to re-derive it from git blame:
- apps/journal/vite.config.ts — expanded the comment near the
basic-ssl plugin to spell out:
* What works on HTTP (everything except Wahoo)
* What needs HTTPS=1 (Wahoo OAuth specifically)
* The norm: don't add new HTTPS-only paths without writing them
down here too, so the assumption stays auditable
- apps/journal/.env.example — new tracked file showing the env vars
the journal app reads, with `ORIGIN=https://localhost:3000`
marked as HTTPS-only and accompanied by an explanation of the
ORIGIN/Playwright mismatch trap. Future contributors don't have
to discover this through a failing e2e run.
- CLAUDE.md — new "Local HTTPS dev (rare)" subsection under
Development Commands. Includes the exact command for Wahoo testing
(`HTTPS=1 ORIGIN=https://localhost:3000 pnpm --filter
@trails-cool/journal dev`), the turbo-doesn't-pass-HTTPS gotcha,
and the rule of thumb: don't set ORIGIN in your .env unless you
also always run with HTTPS=1.
No code/behavior changes; documentation only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>