# Web Engineering Recommendations — September 2026

Authoritative **browser floors**, **PWA**, **security**, **performance**, and **accessibility** guidance for the Audio Vault static/PWA shell and its Go API boundary. Product scope remains in [REQUIREMENTS.md](REQUIREMENTS.md); chosen defaults in [DECISIONS.md](DECISIONS.md) (**D-117** browser baseline, **D-102** accessibility, **D-116** owner-only library).

When this document disagrees with [DECISIONS.md](DECISIONS.md), **DECISIONS wins**.

---

## Browser Support (Baseline Widely Available)

**Decision:** [D-117](DECISIONS.md#web-platform-and-browsers) defaults — **[D-130](DECISIONS.md#web-platform-and-browsers)** makes floors **configurable via `.env`** at runtime (not hardcoded in source). Pin **[Baseline Widely available on 2026-09-07](https://web.dev/articles/use-baseline-with-browserslist)** for reproducible builds ([web.dev](https://web.dev/articles/how-to-choose-your-baseline-target), [Browserslist](https://github.com/browserslist/browserslist)).

**Core browsers (required):** Chrome, Edge, Firefox, Safari — per [AV-R10](REQUIREMENTS.md) and chat [050](chat1.md#050--user).

**Minimum versions** (default deploy values from `BROWSER_MIN_*` in [CONFIG.md](CONFIG.md); override via `.env` per [D-130](DECISIONS.md#web-platform-and-browsers)):

| Browser | Minimum | Notes |
|---|---|---|
| Chrome | **121+** | Desktop + Android (`chrome`, `and_chr`) |
| Edge | **121+** | Chromium-aligned |
| Firefox | **123+** | Desktop + Android (`firefox`, `and_ff`) |
| Safari (macOS) | **17.4+** | |
| Safari (iOS/iPadOS) | **17.4+** | PWA install target |

**Repo config:** [`.browserslistrc`](../../.browserslistrc) (must match `WEB_BASELINE_QUERY`), `WEB_BASELINE_*`, and `BROWSER_MIN_*` in [CONFIG.md](CONFIG.md). When changing floors, update `.env`, `.browserslistrc`, and DECISIONS if defaults change materially.

**Runtime gate:** Go API and PWA read configured floors at startup — no version literals in application code ([D-130](DECISIONS.md#web-platform-and-browsers)).

**Validation:** [AV-T12](TODO.md) failure matrix and [AV-T05](TODO.md) calculator matrix must record **browser + version** rows at or above these floors (or document a waiver in [VALIDATION.md](VALIDATION.md)). **Sign-off:** real installed browsers only ([D-129](DECISIONS.md#web-platform-and-browsers)) — automation may assist, but rows must name a real browser/version or attach a screen recording.

**Unsupported browsers:** Block signup/upload with a clear upgrade message ([D-137](DECISIONS.md#web-platform-and-browsers), `BROWSER_UNSUPPORTED_POLICY=block`); do not silently degrade owner-only playback ([D-116](DECISIONS.md#legal-policies-and-accessibility)).

**Freeze sign-off matrix ([D-137](DECISIONS.md#web-platform-and-browsers)):** Latest **stable** Chrome, Edge, Firefox, Safari on **desktop + mobile** (≥ floors above).

---

## Progressive Web App (PWA)

Follow [web.dev Learn PWA](https://web.dev/learn/pwa) and [MDN PWA guides](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps).

| Topic | Recommendation | Audio Vault constraint |
|---|---|---|
| **Manifest** | `name`, `short_name`, `start_url`, `scope: "/"`, `display: standalone`, icons 192+512, `theme_color`, `background_color` | Scope entire apex `https://audiovau.lt/` ([D-16](DECISIONS.md#infrastructure-and-operations)) |
| **Service worker location** | Register at **site root** (`/sw.js`), not under `/js/` | Cloudflare Pages must serve `Service-Worker-Allowed: /` if SW is not at root ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Service-Worker-Allowed)) |
| **SW role** | Cache shell assets; offline **UI** only | **Do not** use SW as background uploader ([PLAN.md](PLAN.md) Phase 2) — browser lifecycle limits uploads |
| **Updates** | `skipWaiting` + client reload prompt after deploy | Preserve upload state in IndexedDB/server manifest, not SW memory alone |
| **Install** | `beforeinstallprompt` or platform instructions | Record install evidence in [AV-T18](TODO.md) |
| **iOS** | Add to Home Screen; test **standalone** + in-tab | Safari engine only — no alternate in-app browsers |

---

## Security and Privacy (Web Platform)

| Control | September 2026 practice | Implementation |
|---|---|---|
| **HTTPS** | TLS 1.2+ everywhere; HSTS on apex and API | Cloudflare Full (strict) on apex and **`api.audiovau.lt`** ([D-121](DECISIONS.md#web-platform-and-browsers), [AV-T21](TODO.md)) |
| **Cookies (session)** | `HttpOnly`, `Secure`, `SameSite=Lax` (or `Strict` if no cross-site needs) | Go session cookies only; no secrets in `localStorage` |
| **CORS** | Allowlist `APP_URL`; add Pages **preview** origins when demoing from preview ([D-118](DECISIONS.md#web-platform-and-browsers), [D-154](DECISIONS.md#web-platform-and-browsers)) | `CORS_ALLOWED_ORIGINS`, `CORS_PREVIEW_ORIGINS` in [CONFIG.md](CONFIG.md); localhost when `APP_ENV=development` ([D-148](DECISIONS.md#web-platform-and-browsers)) |
| **CSP** | Default-src `'self'`; narrow `connect-src` to **`API_PUBLIC_URL`** + R2 presign hosts; no `unsafe-inline` in production | Start report-only in staging; tighten for beta |
| **Subresource Integrity** | SRI on any third-party scripts | Prefer **no** third-party scripts on apex |
| **Permissions-Policy** | Deny geolocation, microphone, camera unless required | Upload uses file pickers, not camera |
| **Referrer-Policy** | `strict-origin-when-cross-origin` | |
| **Audio delivery** | Short-lived **R2 presigned URLs** with `Range`; Go API authorizes only ([D-119](DECISIONS.md#web-platform-and-browsers)) | [D-17](DECISIONS.md#infrastructure-and-operations), [D-116](DECISIONS.md#legal-policies-and-accessibility) owner-only |
| **Fetch** | `credentials: 'include'` only to API origin; abort via `AbortController` | Calculator already uses this pattern |

Passkeys ([D-20](DECISIONS.md)): follow [WebAuthn best practices](https://web.dev/articles/webauthn) — `rpId` = registrable domain, secure context required. **Disabled through code freeze** ([D-138](DECISIONS.md#authentication-and-sessions), [D-152](DECISIONS.md#authentication-and-sessions)); freeze auth is **password + magic link** ([D-144](DECISIONS.md#authentication-and-sessions)).

---

## Performance (Core Web Vitals)

Target **75th percentile** “good” thresholds per [web.dev Web Vitals](https://web.dev/articles/vitals) (INP replaced FID in 2024):

| Metric | Good | Poor |
|---|---|---|
| **LCP** (load) | ≤ 2.5 s | > 4.0 s |
| **INP** (responsiveness) | ≤ 200 ms | > 500 ms |
| **CLS** (layout stability) | ≤ 0.1 | > 0.25 |

**Product-specific guidance:**

- **Shell first:** ship minimal JS on first paint; defer catalog/upload bundles ([PLAN.md](PLAN.md) Phase 2).
- **Upload UI:** progress updates must not block main thread — batch DOM updates; use `requestAnimationFrame` or virtual lists for large queues.
- **Playback:** stream via **range requests**; do not buffer whole FLAC/DSD in memory.
- **Images:** cover art — explicit `width`/`height` or aspect-ratio to protect CLS.
- **Measure:** Lighthouse CI or RUM on Cloudflare Pages preview ([AV-T36](TODO.md)); log results in [VALIDATION.md](VALIDATION.md).

---

## Accessibility

**Decision:** [D-102](DECISIONS.md) — **WCAG 2.1 Level AA** from first public beta; best-effort at freeze demo.

| Area | Practice |
|---|---|
| **Structure** | One `<h1>` per view; logical heading order; landmark regions (`main`, `nav`) |
| **Focus** | Visible focus rings; trap focus in modals; restore focus on close |
| **Forms** | `<label>` for every control; announce errors with `aria-live="polite"` |
| **Upload queue** | State in text, not color alone (paused, failed, complete) |
| **Player** | Keyboard controls; `aria-label` on buttons; respect `prefers-reduced-motion` |
| **Simulated streamer** | Clearly labeled — not real hardware audio ([D-110](DECISIONS.md#streamer-and-version-2)) |

Test with keyboard-only navigation and VoiceOver/NVDA smoke checks before beta.

---

## Uploads (Browser APIs)

| API | Use | Fallback |
|---|---|---|
| `<input type="file" webkitdirectory>` | Folder pick with relative paths | Reselect folder on resume ([chat 051](chat1.md#051--assistant--final)) |
| `showDirectoryPicker()` | Persistent directory handle where supported | Graceful fallback to one-shot folder input |
| **IndexedDB** | Local upload progress + manifest cache | Server-side authoritative state ([AV-R18](REQUIREMENTS.md)) |
| **Multipart to R2** | Direct `fetch`/`XHR` to presigned URLs | Never embed S3 credentials in static assets |

Concurrency defaults: [D-51](DECISIONS.md) (`UPLOAD_CONCURRENCY=3`, max 6). Test all four browsers in [AV-T12](TODO.md).

---

## Media Playback

| Topic | Recommendation |
|---|---|
| **Capability detection** | Probe `canPlayType` / MediaSource before promising in-browser playback |
| **Formats** | MP3/AAC/FLAC/WAV vary by browser; **DSD never in-browser** — honest unsupported message + simulated streamer path ([D-140](DECISIONS.md#web-platform-and-browsers), [AV-T17](TODO.md)) |
| **Range requests** | Required for seek; presigned URLs must allow `Range` ([D-119](DECISIONS.md#web-platform-and-browsers)) |
| **Future real streamer** | Same presigned original-byte path via authenticated adapter; simulated streamer at freeze has no real hardware audio ([D-110](DECISIONS.md#streamer-and-version-2)) |
| **Media Session API** | Optional lock-screen metadata for installed PWA |
| **Owner-only** | Playback URLs scoped to authenticated session; no anonymous streams ([D-116](DECISIONS.md#legal-policies-and-accessibility)) |

---

## Static Hosting (Cloudflare Pages)

Deploy and preview steps: [PAGES-DEPLOY.md](PAGES-DEPLOY.md) (AV-T36).

- Serve `docs/` (or build output) from apex; public Pages **preview URLs** OK for freeze demo ([D-146](DECISIONS.md#infrastructure-and-operations), [D-150](DECISIONS.md#infrastructure-and-operations)). API on **`https://api.audiovau.lt`** ([D-121](DECISIONS.md#web-platform-and-browsers)), separate from Pages ([D-16](DECISIONS.md#infrastructure-and-operations)–[D-17](DECISIONS.md#infrastructure-and-operations)).
- Set security headers via `_headers` or Cloudflare Transform Rules.
- Cache immutable hashed assets long; `index.html` / `sw.js` short TTL or `no-cache`.
- Do **not** place customer audio on Pages ([D-17](DECISIONS.md#infrastructure-and-operations)).

---

## Go API (Servarica + Cloudflare)

| Topic | Decision |
|---|---|
| **Public URL** | `API_PUBLIC_URL` = `https://api.audiovau.lt` ([D-121](DECISIONS.md#web-platform-and-browsers)) |
| **DNS / TLS** | `api` A/AAAA or CNAME to Servarica origin; Cloudflare proxy; **Full (strict)** |
| **Health** | Liveness **`GET /health`**; readiness **`GET /ready`** (Postgres + R2 checks) |
| **Stripe webhooks** | **`POST {API_PUBLIC_URL}/webhooks/stripe`** (`STRIPE_WEBHOOK_PATH`) |
| **CORS** | Credentialed requests from apex + configured preview origins ([D-118](DECISIONS.md#web-platform-and-browsers), [D-154](DECISIONS.md#web-platform-and-browsers)) |
| **Staging** | Same host, different `.env` deploy; sequential swap on `api.audiovau.lt` ([D-153](DECISIONS.md#infrastructure-and-operations), [D-147](DECISIONS.md#infrastructure-and-operations)) |

---

## References (September 2026)

| Topic | Source |
|---|---|
| Baseline + Browserslist | [web.dev — Use Baseline with Browserslist](https://web.dev/articles/use-baseline-with-browserslist) |
| Choose a Baseline target | [web.dev — How to choose your Baseline target](https://web.dev/articles/how-to-choose-your-baseline-target) |
| Core Web Vitals | [web.dev — Web Vitals](https://web.dev/articles/vitals) |
| PWA | [web.dev — Learn PWA](https://web.dev/learn/pwa) |
| Service workers | [MDN — Using Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) |
| Web app manifest | [web.dev — Add a web app manifest](https://web.dev/articles/add-manifest) |
| WCAG 2.1 | [W3C WCAG 2.1](https://www.w3.org/TR/WCAG21/) |

---

## Related Documents

- [DECISIONS.md](DECISIONS.md) — D-102, D-116, D-117, D-118, D-119, D-121, D-129, D-130, D-137, D-138, D-140, D-146, D-148, D-150, D-152, D-154
- [CONFIG.md](CONFIG.md) — `APP_URL`, `API_PUBLIC_URL`, `WEB_*`, `BROWSER_*`, `CORS_*`, `AUTH_*`
- [PLAN.md](PLAN.md) — Phases 2–4 web/PWA work
- [TODO.md](TODO.md) — AV-T05, AV-T12, AV-T17, AV-T18, AV-T36
