Developer Docs

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 an OlmMachine bound to the user's matrixUserId (@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 EncryptedPayload holds the algorithm, ciphertext, sender_key, session_id, and device_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 the encryptedBody with the decryptedBody seamlessly.
  • WebSocket Heartbeats: An explicit ping is 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-request include 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.

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 nodes cross-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

  1. Collapsed: 56px circle with message icon + pulsing unread dot
  2. Expanded: 360ร—480px drawer with E2EE conversation
  3. Auto-creates a SUPPORT-type conversation scoped to the current maletId
  4. Only renders for authenticated visitors (not in preview/designer mode)
  5. 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:

  1. A DELETE_MESSAGE GraphQL mutation is fired
  2. The backend sets deleted_at and deleted_by on the message row
  3. A message_deleted WS event is broadcast to all participants
  4. All clients replace the message body with "This message was deleted" and render it as a SYSTEM message 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