# mirror — Master Plan > **This file is the source of truth.** Anything marked `LOCKED` may not be > changed without an explicit decision recorded here first. Phases implement > this plan; they do not revise it. ## 1. Vision Mirror a screen from any device that has a browser, and watch every screen that anyone is mirroring, from any other device — with nothing to install and nothing to sign in to. Day one, one command: ```bash uv run mirror serve ``` * `/mirror` — pick a window, screen, or tab using the **operating system's own** picker, and mirror it. * `/watch` — see every live mirror as a wall of tiles; click one to take it over the whole screen. * N sharers and M watchers at the same time, in any combination. The single thing that must never regress: **a person on a Linux laptop, a person on a phone, and a person on a work machine can all join by opening a URL.** ### Design principles 1. **The browser is the whole client.** No native helper, no extension, no WebAssembly to fetch, no build step for the frontend. 2. **Media never touches the server.** The server is a roster and a signaling relay. It handles JSON, always small, never frame data. 3. **Linux is the reference platform.** X11/Xorg is the guaranteed path; the Wayland portal path is supported where the compositor provides it. Every capture-related change is verified on Linux before anything else. 4. **Zero-configuration on a LAN.** STUN/TURN are opt-in, not defaults: peers on one network reach each other with host candidates, and shipping STUN by default would leak internal addresses to a third party. 5. **The prototype is the seed, not a spike.** Code in `src/mirror/` is production-shaped: typed, linted, covered, and gated by the same test suite future phases must keep green. ## 2. Architectural anchors | # | Component | Decision | Rationale | Status | |---|-----------|----------|-----------|--------| | A1 | Language / runtime | Python **3.13+** (developed on 3.14), `uv` for everything | Matches the estate; `uv` is the only installer | **LOCKED** | | A2 | HTTP + signaling | **FastAPI + uvicorn[standard]**, single process | Native WebSocket support, typed boundary, one dependency tree | **LOCKED** | | A3 | Media transport | **WebRTC, mesh, one `RTCPeerConnection` per viewer** | Hardware codecs, sub-100ms LAN latency, no server media handling; server stays a roster | **LOCKED** | | A4 | Who negotiates | The **sharer is always the offerer**; the viewer only answers | Removes renegotiation glare; the sharer owns the track set | **LOCKED** | | A5 | Wire format | **JSON over one WebSocket** at `/ws`, camelCase keys, discriminated on `type` | One connection per peer, trivially debuggable with `websocat` | **LOCKED** | | A6 | Validation | **Pydantic models with `alias_generator=to_camel`** and `extra="ignore"` | camelCase on the wire, snake_case in Python. Explicit aliases are mandatory: silent field-dropping is a bug we already shipped once | **LOCKED** | | A7 | State | **In-memory roster only** (`mirror.registry.Registry`). No database, no ORM, no Alembic | A mirror that outlives the process is a lie: the peers are gone. Persistence would add a failure mode and remove none | **LOCKED** | | A8 | Domain/transport split | `Registry` is pure and returns `list[Envelope]`; `Hub` owns sockets | The whole admission/teardown policy is unit-testable without a socket | **LOCKED** | | A9 | Frontend | **Hand-written ES modules**, no framework, no bundler | Two pages and one WebSocket do not earn a build pipeline; `node --check` is the whole toolchain | **LOCKED** | | A10 | Assets | **No external CDNs, fonts, or analytics.** Everything served from `/static` | Works offline on a LAN; nothing phones home; nothing to time out | **LOCKED** | | A11 | Authentication | **None**, deliberately | Trusted-LAN tool, per the product brief. Threat model lives in `SECURITY.md` | **LOCKED** | | A12 | Configuration | Environment variables (`MIRROR_*`) + optional `.env` | Twelve-factor, container-friendly, no config file format to invent | **LOCKED** | | A13 | E2E testing | **Playwright + full Chromium** (`channel="chromium"`), headless, with a real frame-content assertion | See §5. The assertion is on decoded pixels, never on log lines | **LOCKED** | | A14 | Linux-native capture test | Separate `realdisplay` marked suite: private **Xvfb** + headed Chromium pinned to `--ozone-platform=x11` | Proves the real capture backend without touching the developer's session | **LOCKED** | | A15 | CLI | `argparse`, single `serve` subcommand, flags also accepted bare | One command; no dependency for a two-option CLI | **LOCKED** | | A16 | Quality gates | `ruff check` + `ruff format --check` + `pyright` (0 errors) + `pytest` green, always | Non-negotiable before a phase is called complete | **LOCKED** | | A17 | Scale-out beyond a mesh | *Proposed*: WebSocket/MSE relay or WebCodecs SFU when a sharer's uplink, not the CPU, is the limit | Revisit only with a measured problem. Status: **PROPOSED** | Proposed | | A18 | TLS (standalone) | *Proposed*: `mirror serve --https` with an auto-generated self-signed cert | Still open for a bare `mirror serve`. The problem it existed to solve is solved in production by A19's deployment, which terminates TLS in nginx. Status: **PROPOSED** (deprioritised) | | A19 | TLS + admission in production | TLS terminates in **nginx on the service VM** (no gateway, no `proxy_protocol`); access is decided by **source address** at both edges — nginx `allow`/`deny` and `MIRROR_ALLOW_CIDRS`, with `X-Forwarded-For` believed only from `MIRROR_TRUSTED_PROXIES` (loopback) | There are no accounts, so the address *is* the access control; the app repeats the decision because a container port can be reached without the proxy. Over HTTPS every device gets `getDisplayMedia()`, which is what the fleet needs. Deployment lives in `deployments/mirror/` | **LOCKED** | ## 3. Architecture ``` sharer browser watcher browser(s) ┌───────────────┐ ┌───────────────────────┐ │ /mirror │ │ /watch │ │ getDisplayMedia│ │ tile per mirror │ │ ↓ track │ │ ↑ ontrack →