Files
tajniak81andClaude Opus 5.5 829b236efe
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
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
Desktop: notifications, tray, auto-update, installer; web: sliding sync, voice messages, notifications
Web client (also what the desktop app runs):

- Simplified Sliding Sync (MSC4186) when the homeserver advertises
  org.matrix.simplified_msc3575. src/matrix/sliding.ts answers the SDK's
  GET /sync with a sliding sync request and rewrites the response into
  the /sync shape, as the Android app does; the SDK's own sliding sync
  mode doesn't sort thread replies into threads. 20 rooms first, then
  every room (up to 1000); to_device, e2ee, account data, receipts and
  typing extensions; M_UNKNOWN_POS starts a new connection. The SDK's
  /members?at=<sync token> drops the token, which here is ours. Settings
  can switch back to /sync.
- Notifications for new messages and incoming calls while Warp isn't in
  front, by the account's push rules, evaluated after decryption. The
  browser's Notification API once turned on in Settings, or the desktop
  app's bridge (window.warpDesktop). One per room; a click opens it.
- Voice messages: record from the composer, sent as m.audio with
  MSC3245's voice marker and MSC1767's duration and waveform (encrypted
  in encrypted rooms); a waveform player in the timeline.
- @warp/ui: a slot for extra composer buttons; bell-off, play and pause
  icons.

Desktop app:

- System notifications through Wails' notifications service, fed by
  /_desktop/bridge.js (/_desktop/notify, JSON only); clicking one shows
  the window and opens the room.
- Tray icon with Open and Quit; closing the window hides it (a one-time
  notification says so), File > Quit ends it. The page title goes to
  the window and the tray tooltip, the unread count to the taskbar or
  Dock badge.
- Auto-update from a Wails update manifest at DESKTOP_UPDATE_URL: a quiet
  check a minute after start and every six hours, a notification when a
  newer version exists, Help > Check for updates opens the update window.
  Optional signature key (DESKTOP_UPDATE_KEY). Linux package installs
  leave updates to the package manager. build/manifest writes the
  manifest.
- Per-user NSIS installer (make desktop-windows-installer), the macOS
  bundle zipped for the updater, a desktop CI job, and a Desktop release
  workflow that builds all three platforms and the manifest.

Tested on Windows against a local homeserver: sliding sync with E2EE,
threads and a server restart; notifications (toast history), close to
tray, relaunch restoring the window; voice messages recorded, sent and
played, encrypted too; the update check finding a newer version. Not
tested: installing an update, the NSIS installer, CI, macOS and Linux.
Code signing needs certificates and isn't done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:01:02 +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.
  • 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. 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:<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, 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)
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.