# Warp web client A Matrix client for the browser: the room list, the timeline, sending messages and files, invites, creating and joining rooms. It speaks the plain Client-Server API through [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk), so it works against any Matrix homeserver, and it is built on Warp's design system, [`@warp/ui`](../ui/README.md). The client is **optional and separate from the homeserver**, like the [admin panel](../admin/README.md). It ships as its own small binary, `warp-web`, with the built Vue app embedded. A Warp server runs the same whether or not you install it. ``` browser ──► warp-web :8080 ──► warp :8008 serves the client /_matrix/client/* /config.json /_matrix/media/* ``` By default warp-web proxies the two path prefixes a client uses and answers `404` for everything else (federation, server keys, `/_warp/admin`), so it is not a second way into the rest of the homeserver. The browser only talks to warp-web: no CORS, and the homeserver URL is runtime configuration rather than baked into the build. If the homeserver is already public behind its own proxy, set `-public-homeserver` instead. The browser then calls it directly, warp-web proxies nothing, and the Content-Security-Policy allows exactly that one origin. ## What it does - Sign in with a password, and sign up when the homeserver has registration on (with a registration token when it asks for one). "Stay signed in" keeps the session in `localStorage`; without it the session goes with the tab. Signing out logs the session out on the homeserver. - Room list with unread counts and mentions, invites, a filter, direct messages with presence. - Timeline: grouped messages, day separators, room events as one-line notices, older history on scroll, images, video, audio and files. Media is fetched with the access token (authenticated media, Matrix 1.11) and shown from `blob:` URLs. - Sending: text (`/me` for emotes), files by the attach button, paste or drag and drop. Your message shows at once as *Sending*, then *Sent*, then *Read* when someone else has read it; a failed one says *Not sent* with Retry and Delete. - Read receipts, typing notifications, deleting messages. - End-to-end encryption, with the Rust crypto stack matrix-js-sdk loads (`matrix-sdk-crypto-wasm`): reading and sending in encrypted rooms, files included (encrypted in the browser before they are uploaded). A message this session can't decrypt says why, such as "sent before this session signed in", not just "encrypted". - Verification: verify someone's session from the room's member list, or this session from one of your others, by comparing emoji (SAS). Requests from others open a dialog to accept or decline. - Encryption setup in Settings: cross-signing keys, secret storage and a key backup in one step, behind a recovery key shown once. A new session verifies itself with the recovery key (or another session) and restores older messages from the backup. Reset encryption when the key is lost. - New private rooms and direct messages are end-to-end encrypted by default (a direct message only when the other person has a session with keys); Create room has the switch. - Create a room (private, or public with an address), start a direct chat, join by address or from the room directory, accept or decline invites, leave. - Room info: topic, members and their roles, invite, edit name and topic. - Settings: display name, avatar, theme, encryption. - Phone layout: one pane at a time, the room list is the home screen. **Not yet:** per-message trust shields (a message from an unverified session looks like any other), verifying a person's identity in a direct message rather than one session at a time, QR-code verification, a warning when someone's identity changes, encrypted thumbnails for images you send (the receiving side shows the whole image), and a passphrase for the crypto store. After that: replies, reactions, threads, spaces, notifications. ### Encryption keys in the browser Each session keeps its keys in IndexedDB under its own name (`warp-web-crypto::`), unencrypted at rest like any web client's without a passphrase. Signing out deletes them; a session that ends some other way (signed out elsewhere, the token revoked) has them deleted when the page next starts. A session kept only for the tab ("Stay signed in" off) leaves its store behind when the tab closes; the session can't be reused, but the keys stay until site data is cleared. The `.wasm` file is most of the download (about 2 MB compressed) and needs `script-src 'wasm-unsafe-eval'`, which warp-web's Content-Security-Policy allows; it allows no JavaScript `eval`. ## Run it ### With Docker Compose ```bash cd deploy/compose docker compose --profile web up -d ``` The client is on . Leave out `--profile web` and it isn't installed. ### As a container ```bash docker build -t warp-web -f apps/web/Dockerfile . docker run -p 8080:8080 -e WARP_WEB_HOMESERVER=http://warp:8008 warp-web ``` ### As a binary ```bash make web ./bin/warp-web -homeserver http://localhost:8008 ``` | Flag | Environment | Default | |---|---|---| | `-listen` | `WARP_WEB_LISTEN` | `:8080` | | `-homeserver` | `WARP_WEB_HOMESERVER` | `http://localhost:8008` | | `-public-homeserver` | `WARP_WEB_PUBLIC_HOMESERVER` | empty: proxy through warp-web | | `-log-format` | `WARP_WEB_LOG_FORMAT` | `text` (or `json`) | `/healthz` answers `ok` for container health checks. `/config.json` is what the client reads on start: `{"homeserver_url": "", "version": "..."}`, an empty URL meaning "this origin". ## Deploying - Put warp-web behind your TLS proxy. It sends a strict Content-Security-Policy, `X-Frame-Options: DENY` and `Referrer-Policy: no-referrer` with the client's pages. - Keep the proxy's read timeout above 30 seconds: `/sync` long-polls. - Allow request bodies as large as the homeserver's `media.max_upload_bytes`; uploads pass through warp-web. - warp-web adds `X-Forwarded-For`. The homeserver uses it for rate limits only with `client_api.trust_x_forwarded_for` on; without it, every client counts against warp-web's own address. Turn it on only when every client reaches the homeserver through proxies you control. ## Develop ```bash pnpm install pnpm web:dev # http://localhost:5175, proxied to localhost:8008 ``` `WARP_WEB_HOMESERVER` points the dev server at another homeserver. Vite proxies the same paths warp-web does, so the app code is the same in both. | Command | What | |---|---| | `make web-web` | build the frontend into `internal/client/build/dist` | | `make web` | build the frontend, then `bin/warp-web` with it embedded | | `make web-test` | typecheck the frontend, vet and test the Go host | | `make web-docker` | build the container image | A `warp-web` compiled before the frontend is built still runs. It serves a page saying the client is missing. ## Layout | Path | What | |---|---| | `src/matrix/` | everything that touches matrix-js-sdk: the client's lifecycle, the timeline projection, sending, media, encryption (`crypto.ts`, `attachments.ts`) | | `src/stores/` | session, room list and connection state, encryption and verification (`security.ts`), profile, toasts (Pinia) | | `src/views/` | sign-in, sign-up, home, room | | `src/components/` | timeline, message body, room info, dialogs | | `cmd/warp-web/` | the binary | | `internal/host/` | static serving, `/config.json` and the allow-listed proxy | | `internal/client/` | the embedded build | The SDK's objects change in place and emit events. The stores turn those events into a version counter (bumped at most once a frame), and the views are computed from the SDK on each bump. Keeping the SDK behind `src/matrix` and `src/stores` is what will let the desktop app put its Go backend behind the same stores.