# 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. - Replies: *Reply* on a message puts it above the composer, and the reply quotes it (sender and first line); the quote goes to the message. Replies are sent without the old quoted fallback in their body (Matrix 1.13), and the fallback in older ones is not shown. - Reactions: six quick ones, or any other emoji typed in, from a message's *React* button; chips under the message with counts, yours highlighted. A chip adds your reaction or takes it back, and says who reacted. - Threads: *Reply in thread* opens the thread beside the room, its root and replies, with a composer that writes into it. Replies stay out of the room's timeline; the root shows "N replies", the last reply's sender and time. *Threads* in the room header lists the room's threads (`/threads`). Read receipts are per thread (MSC3771): the room is marked read with `thread_id` "main", a thread with its root's ID, so reading one leaves the others unread. Unread counts come per thread from the homeserver (MSC3773) and threads with replies you haven't read have a dot, on the root, in the list and on the *Threads* button. - Spaces: when you're in any, the top of the room list picks *All chats* or one space, which narrows the list to its rooms and its subspaces'. A space's page (`/hierarchy`) lists its rooms and subspaces with *Open* or *Join*; its admins add rooms you're in, create new ones in it, and take rooms out. *New space* in the + menu and in the picker. Invites to spaces show with the others. - 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, with an encrypted thumbnail for large images). A message this session can't decrypt says why, such as "sent before this session signed in", not just "encrypted". - Trust on every message: one from a session you haven't verified says *Not verified*; one with something wrong (an unsigned session, mismatched keys, plain text in an encrypted room) says what. - Verification: verify a person once (in a room you share, your direct message first) and every session they verify is trusted; or one session at a time; or this session from one of your others. Either side scans the other's QR code (the camera, in the browser) or both compare emoji. - Warnings when someone's identity changes: a reset since you first saw them asks you to acknowledge it; a reset since you *verified* them says their sessions are no longer trusted, with Verify again or Withdraw verification. - Voice and video calls in any room, all through a LiveKit SFU (MatrixRTC, the Element Call model; the old Matrix 1:1 VoIP is not used). Voice and video call buttons in the room header. In a room with one other person the call rings them: an incoming-call card with ring tone, answer, answer without video and decline, and "Calling…" with a ringback tone for the caller until they answer, decline or a minute passes. A call of two shows the other person large and you in the corner; a bigger one is a grid with a tile per person, their camera or screen, and a ring around whoever is speaking. Mute, camera, screen sharing, leave, and a panel that stays up while you move between rooms. A "Join call" banner while one is going on in a room you're not in it. In an encrypted room the media is end-to-end encrypted with per-participant keys. The room list says when a room has a call going on, and the timeline says when one started and ended. It needs `client_api.rtc` on the server and a LiveKit JWT service; see [docs/calls.md](../../docs/calls.md). warp-web proxies `/.well-known/matrix/client` for this, and `-call-origins` (`WARP_WEB_CALL_ORIGINS`) lets the browser reach the JWT service and LiveKit through the Content-Security-Policy. - 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 rooms (public ones too) and direct messages are end-to-end encrypted by default; Create room has the switch for turning it off. - Voice messages: the microphone button in the composer (while it's empty) records; the bar shows the time and the level, with discard and send. They go out as `m.audio` with MSC3245's voice marker and MSC1767's duration and waveform, Opus in whatever container the engine records (WebM in Chromium, Ogg in Firefox, MP4 in Safari), encrypted like any file in an encrypted room. Received ones play in the timeline with their waveform; click it to seek. - Notifications for new messages and incoming calls while Warp isn't in front: the browser's, once you turn them on in Settings (the browser asks for permission), and the system's own in the [desktop app](../desktop/README.md). Which messages notify is the account's push rules, the same ones that decide what a phone is told, evaluated once a message is decrypted, so an encrypted one shows its text. One notification per room; clicking it opens the room. - Simplified Sliding Sync (MSC4186) when the homeserver has it (`org.matrix.simplified_msc3575` in `/versions`): the first sync asks for the 20 most recent rooms with a few state events each, then widens to every room, so the room list appears at once on a big account. It runs underneath matrix-js-sdk's ordinary `/sync` loop (`src/matrix/sliding.ts` rewrites each response into the `/sync` shape, as the Android app does), because the SDK's own sliding sync mode doesn't sort thread replies into their threads. Settings → This session says which sync is in use and can switch back to `/sync`. - Settings: display name, avatar, theme, notifications, encryption. - Phone layout: one pane at a time, the room list is the home screen. **Not yet:** editing messages. Notifications only while a tab is open: there is no service worker or Web Push. ### Encryption keys in the browser Each session keeps its keys in IndexedDB under its own name (`warp-web-crypto::`). 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) encrypts its store with a random key that lives with its credentials in `sessionStorage`: when the tab closes the key goes with it, and the store left behind can't be read; it is deleted after two weeks unused. A "Stay signed in" session's store is not encrypted at rest, like any web client's without a passphrase. 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. - Serve it over HTTPS: browsers only give the page a camera (for scanning QR codes when verifying) and WebCrypto on a secure origin. - 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 `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, sliding sync (`sliding.ts`), the timeline projection, sending, voice recording (`voice.ts`), media, encryption (`crypto.ts`, `attachments.ts`) | | `src/stores/` | session, room list and connection state, encryption and verification (`security.ts`), calls (`call.ts`), notifications, profile, toasts (Pinia) | | `src/views/` | sign-in, sign-up, home, room | | `src/components/` | timeline, message body, room info, dialogs | | `cmd/warp-web/` | the binary | | `host/` | static serving, `/config.json` and the allow-listed proxy (shared with [the desktop app](../desktop/README.md)) | | `client/` | the embedded build (shared with the desktop app) | 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.