apps/web: a Matrix client for the browser on Vue 3, Tailwind and matrix-js-sdk, built on @warp/ui. Like the admin panel it is an optional, separate service: warp-web embeds the built app, serves /config.json and proxies only /_matrix/client and /_matrix/media to the homeserver (or, with -public-homeserver, lets the browser call it directly). Sign-in and sign-up with registration tokens, room list, timeline with back-pagination, text, images, video and files over authenticated media, local echo with sent/read/failed states, receipts, typing, invites, creating, joining and leaving rooms, room info, settings, phone layout. End-to-end encryption through the Rust crypto: encrypted messages and files, emoji verification, key backup setup and restore. apps/ui: WRoomItem, WMessageBubble and WComposer from the brand package, and the icons the client needs. syncapi fixes found with the client: - A room joined inside the sync window was sent as the join alone with limited: false, so clients thought they had the whole room. It is now read like an initial sync; a profile change is not mistaken for a join. - A live typing notification was reported on every sync, so every long poll returned at once while anybody typed. Typing now has a position in the sync token and is reported when it changes, including the empty list when the last person stops. Also: Dockerfile, a `web` compose profile, Makefile targets, a CI job with a proxy smoke test, docs. PLAN.md moves out of the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.8 KiB
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 (
/mefor 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:<user>:<session>), 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
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: DENYandReferrer-Policy: no-referrerwith the client's pages. - Keep the proxy's read timeout above 30 seconds:
/synclong-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 withclient_api.trust_x_forwarded_foron; 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 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.