# Server Configuration Reference

The Go API reads runtime settings from environment variables. Copy [.env.example](../../.env.example) to an untracked local `.env`, fill secrets, and never commit populated files or place them under `docs/`.

The static storage calculator does **not** load these variables. [DECISIONS.md](DECISIONS.md) records why each default was chosen. Change defaults only through an explicit product decision update.

## Quick Start

```sh
cp .env.example .env
# Edit .env: DATABASE_URL, SESSION_SECRET, S3 credentials, Stripe keys, metadata API key
```

Byte fields use decimal units unless noted: **1 TB = 1,000,000,000,000 bytes** (decimal TB, consistent with the calculator).

---

## Docker Compose (Local Dev)

Start local Postgres only ([D-136](DECISIONS.md#infrastructure-and-operations)). **Object storage uses Cloudflare R2 in dev and production** — there is no local S3 container.

```sh
docker compose up -d
docker compose ps   # postgres healthy
```

Postgres binds to **localhost only** (`127.0.0.1`). Data persists in volume `audiovault_postgres_data` until you run `docker compose down -v`.

Copy these into your untracked `.env` when using Compose for the database ([docker-compose.yml](../../docker-compose.yml); **dev only**):

```env
DATABASE_URL=postgresql://audiovault:audiovault@127.0.0.1:5432/audiovault?sslmode=disable
```

### R2 (Dev and Production)

Create a **disposable dev bucket** in the Cloudflare dashboard (separate from production). Issue an R2 API token with read/write on that bucket only. Never commit credentials.

```env
STORAGE_PROVIDER=cloudflare-r2
S3_ENDPOINT=https://<CLOUDFLARE_ACCOUNT_ID>.r2.cloudflarestorage.com
# EU: S3_ENDPOINT=https://<CLOUDFLARE_ACCOUNT_ID>.eu.r2.cloudflarestorage.com
S3_REGION=auto
S3_JURISDICTION=us
S3_BUCKET=audiovault-dev
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
```

- **Endpoint (US):** `https://<account_id>.r2.cloudflarestorage.com` ([R2 S3 API](https://developers.cloudflare.com/r2/api/s3/api/))
- **Endpoint (EU):** `https://<account_id>.eu.r2.cloudflarestorage.com` — set `S3_JURISDICTION=eu` and use an EU bucket
- **Region:** `auto` (required by the SDK; not used by R2)
- **Dev vs prod:** use different buckets and API tokens; same endpoint hostname for a given Cloudflare account
- **CORS:** configure the dev bucket for browser uploads from `http://127.0.0.1:8081` and `APP_URL` before AV-T10 qualification ([D-119](DECISIONS.md#web-platform-and-browsers))

Production uses a private R2 bucket on the same account pattern ([D-12](DECISIONS.md#infrastructure-and-operations), [D-17](DECISIONS.md#infrastructure-and-operations)).

---

## Application Core

| Variable | Default | Secret | Description |
|---|---|---|---|
| `APP_NAME` | `Audio Vault` | No | Display name. |
| `APP_URL` | `https://audiovau.lt` | No | Public PWA/static site URL (Cloudflare Pages apex). Runtime-configurable via `.env` only ([D-149](DECISIONS.md#infrastructure-and-operations)). |
| `API_PUBLIC_URL` | `https://api.audiovau.lt` | No | Public Go API base URL (Servarica origin, Cloudflare proxy). Same hostname for staging and production; **`APP_ENV`** selects backend ([D-151](DECISIONS.md#infrastructure-and-operations)). OpenAPI, PWA `connect-src`, Stripe webhooks ([D-121](DECISIONS.md#web-platform-and-browsers)). |
| `APP_ENV` | `development` | No | `development`, `staging`, or `production`. **No separate staging DNS** ([D-147](DECISIONS.md#infrastructure-and-operations)); runtime-configurable via `.env` only ([D-149](DECISIONS.md#infrastructure-and-operations)). Affects logging, CORS localhost ([D-148](DECISIONS.md#web-platform-and-browsers)), and deploy credentials. |
| `HTTP_ADDR` | `:8080` | No | Go listen address. Local dev default **`http://127.0.0.1:8080`** ([D-151](DECISIONS.md#infrastructure-and-operations)). |
| `DATABASE_URL` | *(empty)* | **Yes** | PostgreSQL DSN for accounts, uploads, catalog, billing state. |
| `SESSION_SECRET` | *(empty)* | **Yes** | Session signing key; use a long random value in production. |

---

## Schedule

Launch and code freeze share the same calendar day ([D-01](DECISIONS.md#schedule-acceptance-and-capacity), [D-03](DECISIONS.md#schedule-acceptance-and-capacity)). **Code freeze** is **11:59 PM US Eastern** on `CODE_FREEZE_DATE` (`America/New_York`).

| Variable | Default | Secret | Description |
|---|---|---|---|
| `PUBLIC_LAUNCH_DATE` | `2026-09-10` | No | ISO date (US Eastern calendar day) for public product go-live. |
| `CODE_FREEZE_DATE` | `2026-09-10` | No | ISO date (US Eastern calendar day) for code freeze; must match `PUBLIC_LAUNCH_DATE`. |

---

## Sessions and Authentication

Interview: Q12–Q17. Build order when implementing: **password → magic link → passkeys**. Local dev stack: [D-136](DECISIONS.md#infrastructure-and-operations) (Docker Compose); start before prod infra: [D-135](DECISIONS.md#infrastructure-and-operations).

| Variable | Default | Secret | Allowed values | Description |
|---|---|---|---|---|
| `SESSION_MAX_AGE_DAYS` | `90` | No | positive integer | Maximum rolling session lifetime. |
| `SESSION_IDLE_TIMEOUT_DAYS` | `30` | No | positive integer | Log out after this many idle days. |
| `AUTH_ENABLE_PASSWORD` | `true` | No | `true` / `false` | Email + password signup and login. |
| `AUTH_ENABLE_MAGIC_LINK` | `true` | No | `true` / `false` | Passwordless email magic links. |
| `AUTH_ENABLE_PASSKEY` | `false` | No | `true` / `false` | WebAuthn / passkeys **post-freeze** ([D-138](DECISIONS.md#authentication-and-sessions), [D-152](DECISIONS.md#authentication-and-sessions)). |
| `AUTH_MAGIC_LINK_TTL_MINUTES` | `30` | No | positive integer | Magic-link and password-reset token expiry ([D-145](DECISIONS.md#authentication-and-sessions)). |
| `AUTH_MFA_MODE` | `optional` | No | `optional`, `required`, `off` | TOTP or passkey second factor policy. |
| `AUTH_TENANT_ID` | `audiovault` | No | non-empty string | auth-go tenant identifier for single-tenant Audio Vault. |
| `AUTH_MAGIC_LINK_PATH` | `/auth/magic` | No | path | PWA route appended to `APP_URL` for magic-link emails. |
| `AUTH_PASSWORD_RESET_PATH` | `/auth/reset` | No | path | PWA route appended to `APP_URL` for password-reset emails. |
| `AUTH_LOGIN_MAX_FAILURES` | `5` | No | positive integer | Failed login attempts allowed per window per IP. |
| `AUTH_LOGIN_FAILURE_WINDOW_MINUTES` | `15` | No | positive integer | Rate-limit window for login failures. |
| `AUTH_SIGNUP_MAX_PER_HOUR` | `10` | No | positive integer | New signups allowed per IP per hour. |
| `API_RATE_LIMIT_PER_MINUTE` | `100` | No | positive integer | Authenticated API requests per user per minute. |
| `LOGOUT_CLEAR_MODE` | `session_only` | No | `session_only`, `full` | `session_only` clears auth only; `full` also clears upload state and caches. |

Account recovery (Q15): email self-service **password reset** only; no manual support recovery in v1.

Auth email delivery ([D-131](DECISIONS.md#infrastructure-and-operations), [D-144](DECISIONS.md#authentication-and-sessions)): **Resend** for magic links and password resets from `@audiovau.lt`. Inbound **`abuse@`**, **`privacy@`**, and **`support@`** on Apple iCloud ([D-143](DECISIONS.md#infrastructure-and-operations), [D-156](DECISIONS.md#infrastructure-and-operations)).

| Variable | Default | Secret | Allowed values | Description |
|---|---|---|---|---|
| `AUTH_EMAIL_PROVIDER` | `resend` | No | `resend` | Transactional auth mail via Resend ([D-131](DECISIONS.md#infrastructure-and-operations)). |
| `AUTH_EMAIL_FROM` | `noreply@audiovau.lt` | No | email | From address for magic links and password resets. |
| `RESEND_API_KEY` | *(empty)* | **Yes** | | Resend API key; verify `audiovau.lt` domain in Resend dashboard ([AV-T43](TODO.md)). |

---

## Object Storage

Interview: Q6–Q8. Customer audio stays on **private R2**; never on the apex website (Q55).

| Variable | Default | Secret | Description |
|---|---|---|---|
| `STORAGE_PROVIDER` | `cloudflare-r2` | No | Storage backend identifier. |
| `S3_ENDPOINT` | *(empty)* | No | R2 S3-compatible endpoint URL. |
| `S3_REGION` | *(empty)* | No | Provider region string required by SDK. |
| `S3_JURISDICTION` | `us` | No | Planned data jurisdiction: `us` or `eu`. MVP default US. |
| `S3_BUCKET` | *(empty)* | No | Private bucket for customer originals. |
| `S3_ACCESS_KEY_ID` | *(empty)* | **Yes** | Server-only storage credential. |
| `S3_SECRET_ACCESS_KEY` | *(empty)* | **Yes** | Server-only storage credential. |
| `S3_USE_PATH_STYLE` | `false` | No | R2 uses virtual-hosted-style URLs. Set `true` only for non-R2 S3-compatible fixtures. |
| `STORAGE_PRESIGN_TTL_MINUTES` | `15` | No | Lifetime of presigned upload/playback URLs. |

Embedded file-tag metadata from uploads is handled by the normal upload/catalog pipeline ([D-123](DECISIONS.md#uploads-files-and-integrity)) — not via integration settings flags.

---

## Upload Limits and Validation

Interview: Q26–Q35. Source identity: **SHA-256 + file size + relative path** (Q30; enforced in code, not a separate env var).

| Variable | Default | Secret | Allowed values | Description |
|---|---|---|---|---|
| `UPLOAD_CONCURRENCY` | `3` | No | positive integer | Default parallel file uploads per session. |
| `UPLOAD_MAX_CONCURRENCY` | `6` | No | positive integer | Hard maximum parallel uploads. |
| `UPLOAD_PART_SIZE_BYTES` | `16777216` | No | bytes | Multipart part size (16 MiB default). |
| `UPLOAD_MAX_FILE_BYTES` | `10000000000` | No | bytes | Maximum single file size (10 GB). |
| `UPLOAD_MAX_PATH_DEPTH` | `20` | No | positive integer | Maximum folder nesting depth (path segments). |
| `UPLOAD_MAX_PATH_LENGTH` | `4096` | No | characters | Maximum relative path length. |
| `UPLOAD_PATH_NORMALIZE` | `nfc` | No | `nfc` | Unicode normalization for paths. |
| `UPLOAD_RESUME_TTL_DAYS` | `30` | No | days | How long interrupted uploads remain resumable. |
| `ORPHAN_MULTIPART_RETENTION_DAYS` | `30` | No | days | Delete incomplete multipart uploads after this age. |
| `UPLOAD_ALLOW_CUE` | `true` | No | `true` / `false` | Accept `.cue` sidecars as companions. |
| `UPLOAD_ALLOW_LOG` | `true` | No | `true` / `false` | Accept `.log` sidecars as companions. |
| `UPLOAD_ALLOW_COVER_IMAGES` | `true` | No | `true` / `false` | Accept cover images for catalog only (not playable music). |
| `UPLOAD_COLLISION_POLICY` | `confirm_replace` | No | `confirm_replace` | Same path, different file: replace only after user confirms; else reject. |
| `UPLOAD_CHANGED_SOURCE_POLICY` | `confirm_overwrite` | No | `confirm_overwrite` | Changed bytes at same path: overwrite cloud copy only after confirm. |
| `UPLOAD_REJECT_CORRUPT` | `true` | No | `true` / `false` | Reject zero-byte and corrupt audio with visible errors; no auto-retry. |

---

## Quotas, Trash, and Account Storage

Interview: Q19, Q27, Q41–Q44.

| Variable | Default | Secret | Description |
|---|---|---|---|
| `STORAGE_QUOTA_BYTES` | `2000000000000` | No | **2 TB** included with the paid plan. |
| `ACCOUNT_MAX_STORAGE_BYTES` | `20000000000000` | No | **20 TB** engineering ceiling per account (not the plan cap). |
| `ACCOUNT_MAX_FILES` | `500000` | No | Default deploy ceiling for music files per account; hard-enforced at upload scan ([D-127](DECISIONS.md#account-limits-and-settings)). Per-account override when [D-128](DECISIONS.md#account-limits-and-settings) enabled. |
| `ACCOUNT_MAX_FILES_ENFORCEMENT` | `hard` | No | `hard` (reject at cap), `warn` (allow with warning), or `off` (no file-count cap). MVP: `hard` ([D-127](DECISIONS.md#account-limits-and-settings)). |
| `TRASH_RETENTION_DAYS` | `30` | No | Days deleted library items remain in trash. |
| `TRASH_COUNTS_TOWARD_QUOTA` | `true` | No | Trashed bytes still count against `STORAGE_QUOTA_BYTES`. |
| `ACCOUNT_DELETION_RETENTION_DAYS` | `30` | No | Soft-delete period before purging account data and objects. |
| `ACCOUNT_DELETION_CANCEL_STRIPE_FIRST` | `true` | No | When `true`, deletion wizard cancels Stripe subscription before soft-delete ([D-134](DECISIONS.md#retention-and-account-lifecycle)). |
| `ACCOUNT_LIMITS_USER_EDITABLE` | `true` | No | When `true`, users may lower limits per group in account settings ([D-128](DECISIONS.md#account-limits-and-settings)). |
| `UX_TYPICAL_UPLINK_MBPS` | `25` | No | Typical home upload speed for ETA copy — **single-stream** model ([D-126](DECISIONS.md#account-limits-and-settings)). |
| `UX_UPLINK_SINGLE_STREAM` | `true` | No | When `true`, upload ETA assumes one stream at `UX_TYPICAL_UPLINK_MBPS`. |

Soak-test targets (**2 TB**, **100k files**, all formats) are fixed in [VALIDATION.md](VALIDATION.md#scale-limits-approved-sep-7-2026) ([D-125](DECISIONS.md#account-limits-and-settings)) — not runtime env keys.

Luke’s **10 TB story** is a stress-test scenario, not `STORAGE_QUOTA_BYTES`.

**Seven limit groups** ([D-128](DECISIONS.md#account-limits-and-settings)): `STORAGE_QUOTA_BYTES`, `ACCOUNT_MAX_STORAGE_BYTES`, `ACCOUNT_MAX_FILES`, `UPLOAD_MAX_PATH_DEPTH`, `UPLOAD_MAX_FILE_BYTES`, `UPLOAD_CONCURRENCY`, `UPLOAD_MAX_CONCURRENCY`. Server `.env` sets defaults and ceilings; per-account overrides (when enabled) cannot exceed the server value.

---

## Billing and Stripe

Interview: Q18–Q25, Q49. **Stripe test mode** through freeze; live mode requires explicit authorization.

**Pricing model:** One **USD** price list (`$49` / `$490` annual). Sell **worldwide wherever Stripe supports** checkout; do not maintain separate regional price IDs or `.env` currency variants. Optional local-currency **presentment** (card charges converted at checkout) is a **Stripe Dashboard** setting, not separate application prices.

**Dashboard setup (human, test mode):** Step-by-step product and Price creation, API keys, webhooks, and checkout verification — [STRIPE-SETUP.md](STRIPE-SETUP.md) ([AV-T20](TODO.md)).

| Variable | Default | Secret | Allowed values | Description |
|---|---|---|---|---|
| `STRIPE_MODE` | `test` | No | `test`, `live` | Stripe API mode. |
| `STRIPE_LIVE_REQUIRES_EXPLICIT_AUTH` | `true` | No | `true` / `false` | When `true`, refuse `live` without an ops go-live flag. |
| `STRIPE_SECRET_KEY` | *(empty)* | **Yes** | | Stripe secret key for the active mode. |
| `STRIPE_PUBLISHABLE_KEY` | *(empty)* | No | | Stripe publishable key for Checkout. |
| `STRIPE_WEBHOOK_SECRET` | *(empty)* | **Yes** | | Webhook signing secret. |
| `STRIPE_WEBHOOK_PATH` | `/webhooks/stripe` | No | Path on `API_PUBLIC_URL` for Stripe Dashboard ([D-121](DECISIONS.md#web-platform-and-browsers)). |
| `STRIPE_PRICE_ID` | *(empty)* | No | | Monthly USD Stripe Price ID. |
| `STRIPE_PRICE_ID_ANNUAL` | *(empty)* | No | | Annual USD Stripe Price ID. |
| `SUBSCRIPTION_CURRENCY` | `USD` | No | `USD` | Sole plan currency; worldwide checkout uses these USD prices. |
| `SUBSCRIPTION_PRICE` | `49` | No | major units | Monthly USD price (display + Stripe sync reference). |
| `SUBSCRIPTION_ANNUAL_PRICE` | `490` | No | major units | Annual USD price (~17% off monthly×12). |
| `SUBSCRIPTION_TRIAL_DAYS` | `14` | No | days | Free trial length; card required at signup. |
| `SUBSCRIPTION_GRACE_DAYS` | `7` | No | days | After failed payment before hard blocks escalate. |
| `SUBSCRIPTION_FAILED_PAYMENT_BLOCK_UPLOADS` | `true` | No | `true` / `false` | Block new uploads during grace. |
| `SUBSCRIPTION_FAILED_PAYMENT_ALLOW_PLAYBACK` | `true` | No | `true` / `false` | Allow read/play during grace. |
| `SUBSCRIPTION_AUTO_DELETE_ON_FAILURE` | `false` | No | `true` / `false` | Never auto-delete music on failed payment in v1. |
| `SUBSCRIPTION_CANCEL_AT_PERIOD_END` | `true` | No | `true` / `false` | Portal cancel retains access until period end. |
| `SUBSCRIPTION_PRORATE_REFUND` | `false` | No | `true` / `false` | No pro-rata refunds on cancel. |
| `SUBSCRIPTION_WITHDRAWAL_DAYS` | `14` | No | days | EU/EEA/UK baseline statutory withdrawal window ([terms.md](../legal/terms.md)). |
| `SUBSCRIPTION_WITHDRAWAL_REIMBURSE_DAYS` | `14` | No | days | Reimbursement deadline after valid withdrawal notice ([terms.md](../legal/terms.md)). |
| `LAUNCH_GEO_MODE` | `stripe_supported` | No | `stripe_supported` | Sell wherever Stripe supports; legal review in AV-T22. |

Entitlement must come from **verified server-side Stripe state**, not a checkout redirect alone.

---

## Metadata Identification

Interview: Q36–Q40. If schedule slips, disable auto-match with `METADATA_AUTO_MATCH_ENABLED=false` (Priority cut #1).

| Variable | Default | Secret | Allowed values | Description |
|---|---|---|---|---|
| `METADATA_PROVIDER` | `acoustid+musicbrainz` | No | provider id | Identification stack. |
| `METADATA_AUTO_MATCH_ENABLED` | `true` | No | `true` / `false` | Enable automatic identification jobs. |
| `METADATA_API_URL` | *(empty)* | No | URL | Override provider API base if needed. |
| `METADATA_API_KEY` | *(empty)* | **Yes** | | Provider API key when required. |
| `METADATA_CONFIDENCE_THRESHOLD` | `80` | No | 0–100 | Auto-apply matches at or above this percent; below → review queue. |
| `METADATA_CONSENT_MODE` | `policy_link` | No | `policy_link`, `explicit_consent` | Default privacy policy link at signup; optional one-time scan consent. |
| `METADATA_SEND_FILENAMES` | `true` | No | `true` / `false` | Allow sending paths/filenames to provider. |
| `METADATA_SEND_TAGS` | `true` | No | `true` / `false` | Allow sending embedded tags to provider. |
| `METADATA_SEND_FINGERPRINT` | `true` | No | `true` / `false` | Allow acoustic fingerprint lookup (AcoustID). |
| `METADATA_MANUAL_EDITS_ALLOW_REVERT` | `true` | No | `true` / `false` | Manual catalog edits win; user may revert to auto match. |

---

## Operations, Backups, and Legal Contact

Interview: Q9–Q11, Q51. Inbound mailboxes on Apple iCloud MX ([AV-T43](TODO.md)); outbound auth via Resend ([D-131](DECISIONS.md#infrastructure-and-operations)).

| Variable | Default | Secret | Description |
|---|---|---|---|
| `INCIDENT_RESPONSE_TIMEZONE` | `America/New_York` | No | IANA timezone for business-hours on-call. |
| `INCIDENT_BUSINESS_HOURS_ONLY` | `true` | No | No 24/7 SLA in MVP. |
| `BACKUP_RPO_HOURS` | `24` | No | Recovery point objective for Postgres backups. |
| `BACKUP_RTO_HOURS` | `4` | No | Recovery time objective target. |
| `ABUSE_CONTACT_EMAIL` | `abuse@audiovau.lt` | No | Published abuse/takedown contact ([D-101](DECISIONS.md#legal-policies-and-accessibility), [D-143](DECISIONS.md#infrastructure-and-operations)). |
| `PRIVACY_CONTACT_EMAIL` | `privacy@audiovau.lt` | No | Privacy rights contact ([D-143](DECISIONS.md#infrastructure-and-operations)). |
| `SUPPORT_CONTACT_EMAIL` | `support@audiovau.lt` | No | General product/billing support ([D-156](DECISIONS.md#infrastructure-and-operations)); iCloud inbound via [AV-T43](TODO.md). |

Observability stack (Q9): structured logs, uptime checks, **Sentry**, and **Prometheus/Grafana-class metrics** — configure via deployment tooling outside this file.

---

## Web Platform and Browser Support

Decision: [D-117](DECISIONS.md#web-platform-and-browsers), [D-130](DECISIONS.md#web-platform-and-browsers). Full guidance: [WEB.md](WEB.md). Repo pin: [`.browserslistrc`](../../.browserslistrc) — keep aligned with `WEB_BASELINE_QUERY`.

Runtime browser gate and matrix expectations read these keys from server config (Go) and documented static defaults (PWA). **Do not** embed minimum version integers in application source.

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WEB_BASELINE_DATE` | `2026-09-07` | No | Pin date for Baseline Widely available (reproducible builds). |
| `WEB_BASELINE_QUERY` | `baseline widely available on 2026-09-07` | No | Browserslist query; **must match** [`.browserslistrc`](../../.browserslistrc). |
| `BROWSER_MIN_CHROME` | `121` | No | Minimum Chrome major (desktop/Android). |
| `BROWSER_MIN_EDGE` | `121` | No | Minimum Edge major. |
| `BROWSER_MIN_FIREFOX` | `123` | No | Minimum Firefox major (desktop/Android). |
| `BROWSER_MIN_SAFARI` | `17.4` | No | Minimum Safari version (macOS). |
| `BROWSER_MIN_IOS_SAFARI` | `17.4` | No | Minimum Safari version (iOS/iPadOS PWA). |
| `BROWSER_UNSUPPORTED_POLICY` | `block` | No | `block`, `warn`, or `docs_only` when UA below minimum. |
| `CORS_ALLOWED_ORIGINS` | `https://audiovau.lt` | No | Comma-separated origins for credentialed API calls ([D-118](DECISIONS.md#web-platform-and-browsers)). |
| `CORS_PREVIEW_ORIGINS` | *(empty)* | No | Comma-separated Cloudflare Pages **preview** origins for freeze demo ([D-154](DECISIONS.md#web-platform-and-browsers)); merged with `CORS_ALLOWED_ORIGINS` at runtime when set. |
| `CORS_ALLOW_LOCALHOST` | `true` | No | Allow `http://localhost:*` and `http://127.0.0.1:*` when `APP_ENV=development` ([D-148](DECISIONS.md#web-platform-and-browsers)). |
| `STORAGE_PRESIGN_MODE` | `direct_r2` | No | `direct_r2` (default) or `api_proxy_fallback` after AV-T10 if R2 CORS/range fails ([D-119](DECISIONS.md#web-platform-and-browsers)) |
| `INTEGRATIONS_OAUTH_STUB_ENABLED` | `false` | No | **No** OAuth connect stub in settings at freeze ([D-122](DECISIONS.md#legal-policies-and-accessibility)). |

`EXPORT_CONTACT_EMAIL` and the **`export@`** mailbox stay inactive until public beta ([D-120](DECISIONS.md#legal-policies-and-accessibility)) — not in server `.env` at MVP.

Core Web Vitals targets (public beta): LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 at 75th percentile — see [WEB.md](WEB.md).

---

## Playback and Streamer

Interview: Q57–Q59. v1 streamer is **simulated**; real manufacturer DLC integration is Phase 7 ([D-111](DECISIONS.md#streamer-and-version-2)–[D-115](DECISIONS.md#streamer-and-version-2)) — no DLC env keys at MVP.

| Variable | Default | Secret | Allowed values | Description |
|---|---|---|---|---|
| `STREAMER_MODE` | `simulated` | No | `simulated`, `live` | `simulated` produces no real streamer audio in v1. |
| `STREAMER_QUIET_CONNECT_SINGLE_DEVICE` | `true` | No | `true` / `false` | Phase 7: auto-select when exactly one authorized device. |

---

## Calculator Price Feed

Interview: Q46–Q48. The **static calculator** does not load server `.env`. Price refresh is post-MVP ([D-91](DECISIONS.md#calculator-and-economics)); configure feed paths in calculator hosting when AV-T07 ships — not in this file.

---

## Related Documents

- [DECISIONS.md](DECISIONS.md) — full decision log and interview references
- [WEB.md](WEB.md) — browser Baseline, PWA, security, performance, accessibility
- [REQUIREMENTS.md](REQUIREMENTS.md) — release scope
- [TODO.md](TODO.md) — remaining implementation and legal tasks
- [PLAN.md](PLAN.md) — phased execution
