uChat Client SDK & Interface
The uChat Client SDK brings the power of native-grade, secure messaging directly into the browser. By leveraging WebAssembly (WASM) implementations of the Matrix End-to-End Encryption protocol and Svelte 5 runes, the SDK ensures that conversations between Visitors, Malet Owners, and the Mallnline community remain strictly private while maintaining a robust, reactive user experience.
This document details the frontend implementation (Phase 3 of the uChat roadmap), which shifts cryptographic obligations to the browser so that the backend operates strictly as an encrypted relay.
Architecture at a Glance
| Component | Responsibility | Technology |
|---|---|---|
Store (store.svelte.ts) |
State management, WebSocket lifecycle, API calls | Svelte 5 Runes ($state, $derived) |
Crypto Engine (crypto.ts) |
Key generation, Megolm encryption/decryption | @matrix-org/matrix-sdk-crypto-wasm |
| GraphQL Client | Resolving MY_CONVERSATIONS & SEND_MESSAGE |
GraphQL / urql / fetch |
| WS Relay | Real-time payload delivery over WebSocket | JSON streams + JWT |
E2EE Crypto Engine (`crypto.ts`)
The crypto engine acts as a singleton adapter over the official Matrix Rust SDK compiled to WebAssembly.
- Initialization (
initCryptoEngine): Dynamically imports the WASM bundle to prevent Server-Side Rendering (SSR) crashes. It provisions anOlmMachinebound to the user'smatrixUserId(@uchat_{userId}:localhost). - Device Identity: Every browser session issues a persistent 12-character alphanumeric Device ID stored in
localStorage. - Graceful Degradation: If the WASM engine fails to initialize (e.g., in an unsupported environment), the system safely falls back to a plain-text envelope (
uchat.plaintext.v1), ensuring the system doesn't unexpectedly crash while logging a warning. - Data Encapsulation: The payload structured as
EncryptedPayloadholds thealgorithm,ciphertext,sender_key,session_id, anddevice_id. The uChat server receives and relays this blob opaquely.
Reactive Store (`store.svelte.ts`)
Leveraging Svelte 5 Runes, the uchatStore encapsulates data fetching, WebSocket subscriptions, and symmetric decryption.
Data Flow
// Example: Open a conversation & listen via WebSocket
import { uchatStore } from '$lib/uchat';
await uchatStore.initCrypto(userId);
await uchatStore.openConversation(conversationId);
// UI is now reactive. Incoming WebSocket frames automatically decrypt and map to uchatStore.messages
- Decrypt on Receive: When a WebSocket frame implies an inbound message, the store immediately pipes the serialized event through
decryptMessage(). The derived state (displayMessages) substitutes theencryptedBodywith thedecryptedBodyseamlessly. - WebSocket Heartbeats: An explicit
pingis sent through the WebSocket every 20 seconds. If the socket closes ungracefully, an exponential backoff routine correctly re-establishes the connection and resumes polling as a fallback during reconnection. - Polling Fallback: A 3-second polling interval runs alongside WebSocket connections to ensure message delivery even when WS connections fail (e.g., due to transient gateway auth issues). When a per-conversation WS successfully connects, polling is automatically stopped to reduce HTTP traffic. If WS disconnects, polling resumes seamlessly.
- Error Sanitization: GraphQL errors from
graphql-requestinclude raw response JSON with stacktraces. The store extracts clean, user-facing error messages and auto-clears them after 5 seconds. - PageInfo & Pagination: Infinite scroll loads historical blobs, performing bulk decryption without blocking the Svelte main thread via iterative polling.
UI Components & Design Tokens
The chat interface is styled with platform-native UI tokens (found in ThemeConfig) mapped exactly to the established Malet-centric design system.
Participant Typeahead Search
One of the cornerstones of the "Social Commerce" vision is making it easy to find existing platform members when initiating DIRECT, GROUP, or SOCIAL uChat conversations. The messaging interface employs a robust real-time participant lookup:
- Four-axis Querying: Typeahead queries allow indexing and autocomplete against Username, Display Name, Email, and Phone Number.
- Idempotent Identification: Searches automatically hydrate matching Malet Owners or Visitors, relying on the
nodescross-search to unmask only public profiles or pseudo-anonymous support handles.
Authentication Hydration
For the WebAssembly layer to confidently sign payloads, the authentication state is strictly hydrated down from the +layout.server.ts into the Svelte context before passing user parameters to $lib/uchat/crypto.ts.
On log-out, the $lib/uchat/store.svelte.ts exposes a closeChat() function which explicitly nullifies state and invokes indexedDB.deleteDatabase('uchat_crypto_store') to instantly purge device keysโcrucial for shared devices.
File Reference
| File | Purpose |
|---|---|
src/lib/uchat/crypto.ts |
The core interface dealing directly with the WebAssembly Megolm implementation, generating signatures and providing error boundaries. |
src/lib/uchat/store.svelte.ts |
Complete state-tree housing $state variables and wrapping Svelte lifecycle actions with WebSocket logic. |
src/lib/uchat/queries.ts |
GraphQL operations including MALET_CONVERSATION, MARK_AS_READ, DELETE_MESSAGE |
Checking Health & Engine Status
To assist with UI indications, uchatStore exposes an immutable snapshot getter:
const { ready, userId, deviceId, error } = uchatStore.getCryptoStatus();
Use this block to prompt Visitors manually if encryption isn't supported, rendering a lock icon or a fallback banner if ready equals false.
Visitor Inbox (`/profile/inbox`)
A centralized message hub at /profile/inbox where Visitors see all their conversations in one place.
Features
- Conversation list grouped by type (Support, Direct, Group, Social)
- Type filter chips โ toggle between ALL, SUPPORT, DIRECT, GROUP, SOCIAL
- Unread badges on individual conversations and total count
- Message panel โ select a conversation to read/send messages inline
- Empty states โ friendly prompts to browse Malets
- SSR disabled โ crypto engine requires browser WASM environment
Route Files
| File | Purpose |
|---|---|
src/routes/profile/inbox/+page.ts |
SSR disabled loader |
src/routes/profile/inbox/+page.svelte |
Full inbox UI with filters, conversation list, message panel |
Chat Bubble (Malet Pages)
A floating action button (FAB) anchored to the bottom-right of every Malet page, enabling instant SUPPORT conversations.
Behavior
- Collapsed: 56px circle with message icon + pulsing unread dot
- Expanded: 360ร480px drawer with E2EE conversation
- Auto-creates a
SUPPORT-type conversation scoped to the currentmaletId - Only renders for authenticated visitors (not in preview/designer mode)
- Dynamic import to avoid WASM SSR issues
Integration Point
Injected at the bottom of src/routes/[malet]/+layout.svelte via dynamic import:
{#if $isAuthenticated && malet && !previewConfig && !isDesignerProxy}
{#await import('$lib/components/uchat/ChatBubble.svelte') then ChatBubble}
<ChatBubble.default maletId={malet.id} maletName={malet.name} />
{/await}
{/if}
NOTE
The existing /[malet]/contact page is preserved as a guest fallback (wired to Malet email). The chat bubble is an authenticated-only enhancement.
Unread Tracking
The uchatStore tracks unread message counts per conversation:
| API | Description |
|---|---|
uchatStore.unreadCount |
Total unread across all conversations |
uchatStore.unreadByConversation |
Map<conversationId, count> |
uchatStore.markAsRead(id) |
Clears unread for a conversation |
uchatStore.findMaletConversation(maletId) |
Finds existing SUPPORT conversation for a Malet |
Last-read timestamps are persisted to localStorage under the key uchat_last_read.
Messages Badge (Navigation)
The MessagesBadge.svelte component renders a chat icon with unread count in the global navigation bar (between UCart and NotificationCenter). Clicking navigates to /profile/inbox.
Phase 2: Real-Time Enhancements
WS Token Acquisition Resilience
The issueWsToken mutation routes through the Federation gateway, which validates the session cookie via fetchWithRetry (3s timeout + 1 retry). If the gateway's auth context fails to resolve the user, the mutation will return an authentication error.
The store's acquireWsToken() helper retries the token acquisition with exponential backoff (1s, 2s, 4s) to handle transient gateway load:
async function acquireWsToken(): Promise<string> {
for (let attempt = 0; attempt < 3; attempt++) {
try {
const data = await client.request(ISSUE_WS_TOKEN);
return data.issueWsToken.token;
} catch (err) {
if (attempt === 2) throw err;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
throw new Error('Failed to acquire WS token');
}
Typing Indicators
Typing events are sent via WebSocket text frames as JSON:
// Sent on keystroke (debounced 3s)
ws.send(JSON.stringify({ type: 'typing', state: 'start' }));
// Sent after 3s of inactivity
ws.send(JSON.stringify({ type: 'typing', state: 'stop' }));
The backend relays typing events through Redis pub/sub to other WebSocket listeners. The store maintains a remoteTyping map (userId โ timestamp) and auto-clears stale entries after TYPING_TIMEOUT_MS (3 seconds).
Store API:
uchatStore.sendTypingIndicator(); // Call on every keystroke
uchatStore.typingUsers; // string[] โ currently typing user IDs
Read Receipts
Read receipts are persisted server-side via the markAsRead GraphQL mutation, which updates last_read_message_id and last_read_at on the participant record.
// Mark a conversation as read (fires on conversation open)
uchatStore.markConversationRead(conversationId);
The mutation triggers a read_receipt event broadcast to all other participants via Redis pub/sub โ WebSocket:
// Incoming WS event payload
{
type: 'read_receipt',
conversation_id: 'uuid',
user_id: 'uuid',
message_id: 'uuid'
}
The UI renders โ for sent and โโ (accent-colored) for read on own messages.
Message Deletion
Messages can be soft-deleted by the sender via the deleteMessage mutation. Deleted messages are never physically removed from the database โ the backend substitutes their content at query time.
// Delete a message (sender-only)
await uchatStore.deleteMessage(messageId, conversationId);
On deletion:
- A
DELETE_MESSAGEGraphQL mutation is fired - The backend sets
deleted_atanddeleted_byon the message row - A
message_deletedWS event is broadcast to all participants - All clients replace the message body with "This message was deleted" and render it as a
SYSTEMmessage type
The delete button appears on hover over the sender's own messages (๐ icon).
DM Thread Deduplication
The createConversation mutation for DIRECT conversations checks for existing threads between the same participants before creating a new one. This prevents duplicate 1-on-1 threads when using the "+ New Message" button multiple times.
If an existing DIRECT conversation is found, the mutation returns that conversation instead of creating a duplicate.
Message Attachments
The /uchat page includes a paperclip attachment button that triggers a hidden <input type="file">. The selected file's name and size are sent as an encrypted placeholder message:
๐ invoice.pdf (142.5 KB)
Accepted formats: image/*, .pdf, .doc, .docx, .txt.
Responsive Mobile Layout
The /uchat page uses a swappable panel pattern on viewports โค 768px:
- Sidebar visible: Full conversation list, message area hidden
- Message area visible: Full chat view with back button, sidebar hidden
The showMobileSidebar state controls visibility. Opening a conversation sets showMobileSidebar = false; the mobile back button sets it to true and calls closeChat().
WASM Asset Serving
The Vite configuration includes two optimizations for the Matrix SDK WASM binary:
optimizeDeps: {
exclude: ['@matrix-org/matrix-sdk-crypto-wasm'] // Skip pre-bundling
},
worker: {
format: 'es' // ES module workers for WASM thread compat
}
File Reference
| File | Purpose |
|---|---|
src/lib/uchat/crypto.ts |
WASM Megolm E2EE โ key generation, encrypt/decrypt |
src/lib/uchat/store.svelte.ts |
Reactive store with conversations, messages, WS, unread, typing, receipts |
src/lib/uchat/queries.ts |
GraphQL operations including MALET_CONVERSATION |
src/lib/uchat/index.ts |
Barrel export |
src/lib/components/uchat/ChatBubble.svelte |
Floating chat bubble FAB + inline drawer |
src/lib/components/MessagesBadge.svelte |
Nav icon with unread badge โ /profile/inbox |
src/routes/profile/inbox/+page.svelte |
Visitor inbox โ full conversation hub |
src/routes/profile/inbox/+page.ts |
SSR disabled loader |
src/routes/uchat/+page.svelte |
Full chat UI with typing, receipts, deletion, attachments, mobile layout |
vite.config.ts |
WASM asset serving configuration |
Related
IndexedDB Storage Layer โ Persistence backend for uChat unread timestamps
Wishlists & Collections โ Another IndexedDB-backed client feature
Privacy & Security APIs โ Content visibility and data control
Community Features โ Social conversation infrastructure
Handle System & Sigil Taxonomy โ Pipe sigil taxonomy used for sender handles in uChat conversations
Support Ticket Anonymity โ Pseudo-anonymous alias system protecting Visitor identity in SUPPORT conversations
Link Previews โ Server-side OG scraping and frontend preview card rendering for URLs in messages
uChat Online Presence โ Architecture for real-time connection status tracking.
uChat Voice Messages Architecture โ Technical implementation of E2EE voice messaging.
uChat Message Search (E2EE) โ Hybrid search architecture combining in-memory and IndexedDB caching.