Files
Warp/apps/web
tajniak81andClaude Opus 5.5 8ff2730676
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
Web client: replies, reactions, threads and spaces
- Replies: Reply on a message puts it above the composer; the reply
  quotes it (sender and first line), and the quote goes to the message.
  Sent without the quoted fallback (Matrix 1.13); the fallback in older
  replies is not shown.
- Reactions: the six quick ones of the Android app or any other emoji,
  chips under the message with counts, yours highlighted; a chip adds
  yours or takes it back.
- Threads: matrix-js-sdk's thread support is on. Reply in thread opens
  the thread beside the room with its own composer; the root shows
  "N replies" and the last reply; Threads in the header lists them.
  Receipts are per thread (MSC3771): the room is marked read with its
  read marker and a separate receipt for "main", since /read_markers'
  receipt has no thread_id and would read every thread. Unread counts
  per thread (MSC3773) and a dot on unread threads.
  Two SDK quirks worked around: the thread view creates the thread
  object when a thread starts here (else the first reply's remote echo
  is dropped until reload), and the reply count is re-read from the
  root's bundled count after replies arrive (the SDK can count one
  twice).
- Spaces: kept out of the room list; a picker narrows the list to a
  space's rooms and its subspaces'. A space's page (/hierarchy) lists
  its rooms with Open or Join; admins add existing rooms, create new
  ones in it and take rooms out. New space in the + menu and picker.
- The room composer moved into RoomComposer.vue, shared by the room
  and the thread pane. New thread and space icons in apps/ui.
- Dev server: the Rust crypto's .wasm was lost to vite's pre-bundling,
  so E2EE never started under `pnpm web:dev`; the package is now served
  as it is in dev.

Server: /versions lists v1.1 to v1.14. matrix-js-sdk checks for the
exact version a feature came in, and with only v1.13 and v1.14 it kept
threads, per-thread unread counts, recursive relations and intentional
mentions off.

Tested in the browser against a local server: replies, reactions,
threads (new and existing, in plain and encrypted rooms, receipts
checked on the server) and spaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:09:12 +02:00
..

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, so it works against any Matrix homeserver, and it is built on Warp's design system, @warp/ui.

The client is optional and separate from the homeserver, like the admin panel. 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. 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.
  • Settings: display name, avatar, theme, encryption.
  • Phone layout: one pane at a time, the room list is the home screen.

Not yet: editing messages, notifications.

Encryption keys in the browser

Each session keeps its keys in IndexedDB under its own name (warp-web-crypto:<user>:<session>). 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

cd deploy/compose
docker compose --profile web up -d

The client is on http://localhost:8080. Leave out --profile web and it isn't installed.

As a container

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

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

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, the timeline projection, sending, media, encryption (crypto.ts, attachments.ts)
src/stores/ session, room list and connection state, encryption and verification (security.ts), calls (call.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
host/ static serving, /config.json and the allow-listed proxy (shared with the desktop app)
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.