This document outlines the architecture for the Forward Email web, desktop, and mobile applications, all unified under the Tauri v2 framework.
The core principle is to reuse the Svelte 5 + Vite web application as the UI layer for all platforms. Tauri provides the cross-platform shell, using a Rust backend for native capabilities and system webviews for rendering the UI.
graph TD
subgraph WebApp["Web App (Svelte 5 + Vite)"]
A[UI Components] --> B[Stores]
B --> C{API Client}
C --> D[REST API]
B --> E{WebSocket Client}
E --> F[Real-time API]
end
subgraph TauriShell["Tauri (Desktop & Mobile)"]
G[Rust Backend] --> H{System WebView}
H -- loads --> A
I[Tauri IPC] -- JS Bridge --> A
G -- IPC --> I
J[Tauri Plugins] --> G
end
D --- M[api.forwardemail.net]
F --- M
- UI: The existing Svelte 5 components, stores, and styles are the foundation for all platforms.
- API Client: The existing
ky-based REST client handles standard API requests. - WebSocket Client: A dedicated WebSocket client (
src/utils/websocket-client.js) handles real-time updates.
src-tauri/: The Rust backend for the application.main.rs/lib.rs: The Rust entry points. This is where the Tauri application is configured, plugins are registered, and native IPC commands are defined.tauri.conf.json: The central configuration file for all Tauri features, including window setup, plugin configuration, bundle identifiers, and security settings.Cargo.toml: Manages Rust dependencies, including Tauri and its plugins.
- Tauri IPC: The Inter-Process Communication bridge that allows the Svelte frontend to securely call Rust functions. All exposed Rust functions are explicitly defined with the
#[tauri::command]attribute. - Tauri Plugins: We leverage several official Tauri plugins to provide native functionality:
tauri-plugin-updater: For automatic background updates on desktop.tauri-plugin-notification: For native desktop and mobile push notifications.tauri-plugin-deep-link: To handleforwardemail://custom protocol URLs.tauri-plugin-single-instance: Ensures only one instance of the desktop app can run.tauri-plugin-window-state: Persists window size and position on desktop.
To keep the core application logic clean and reusable, we use several bridge modules:
src/utils/platform.js: A simple utility to detect the current runtime environment (isTauri,isWeb,canUseServiceWorker).src/utils/tauri-bridge.js: Initializes all Tauri-specific event listeners and frontend functionality.src/utils/notification-bridge.js: A wrapper that uses either the native Tauri notification plugin or the web Notifications API, depending on the platform.src/utils/sync-bridge.js: The entry point for our offline sync mechanism (see below).
Since Service Workers are not supported in Tauri's webview, we've implemented a custom "sync shim" to provide offline functionality across all platforms.
-
src/utils/sync-core.js: This is a platform-agnostic module containing the core logic for handling API synchronization and processing a mutation queue from IndexedDB. It is designed as a factory function that accepts an environment object (fetch,indexedDB,postMessage). -
public/sw-sync.js: This is the Service Worker adapter. It runs only in the web version. It importssync-core.jsand provides the Service Worker environment bindings (e.g.,self.fetch,self.indexedDB, andself.clients.matchAll().then(...)forpostMessage). It is triggered by thesyncandperiodicsyncevents. -
src/utils/sync-shim.js: This is the main-thread replacement for the Service Worker, used in all Tauri builds. It also importssync-core.jsbut provides main-thread environment bindings (window.fetch,window.indexedDB, and aCustomEvent-basedpostMessage). It is triggered byonlineevents,visibilitychangeevents, and a simplesetIntervalheartbeat. -
src/utils/sync-bridge.js: This is a unified module that detects the platform at runtime. It initializes either the Service Worker (on the web) or the sync shim (in Tauri). The rest of the application interacts only with this bridge, making all calls to the sync layer platform-agnostic.
This architecture allows us to share the exact same complex synchronization logic between the web and native apps, ensuring consistent behavior and reducing code duplication.
The application provides an optional App Lock feature that encrypts all locally stored data at rest.
-
src/utils/crypto-store.js: The core encryption module. Implements envelope encryption using libsodium (XSalsa20-Poly1305viacrypto_secretbox). A random Data Encryption Key (DEK) encrypts all data. The DEK is itself encrypted by a Key Encryption Key (KEK) derived from the user's PIN (via Argon2id) or passkey (via WebAuthn PRF + HKDF). ProvidesencryptRecord()/decryptRecord()functions for the database layer, andencryptSensitiveLocal()/decryptSensitiveLocal()for localStorage values. -
src/utils/passkey-auth.js: WebAuthn passkey registration and authentication using@passwordless-id/webauthn. Supports the PRF extension for deriving encryption keys directly from the authenticator. Falls back to PIN-based KEK derivation when PRF is unavailable.
-
src/svelte/LockScreen.svelte: An iOS-style lock screen overlay with a numeric PIN pad and passkey authentication button. Renders as a full-viewport overlay at z-index 999999, blocking all app interaction until unlocked. Includes shake animation on wrong PIN and exponential backoff lockout. -
src/svelte/AppLockSettings.svelte: The settings UI in Privacy & Security tab (above PGP encryption). Allows enabling/disabling App Lock, setting up PIN or passkey, configuring inactivity timeout, and changing the PIN. -
src/utils/inactivity-timer.js: Monitors user activity (mouse, keyboard, touch, scroll) and triggers auto-lock after the configured timeout. Supports configurable timeouts from 1 minute to 1 hour.
- On first setup, a random DEK is generated and encrypted with the KEK derived from the user's PIN.
- On app start, if App Lock is enabled, the lock screen is shown before any content renders.
- On successful unlock, the KEK is derived, the DEK is decrypted, and held in a module-scoped closure.
- All IndexedDB writes pass through
encryptRecord()which encrypts non-indexed fields with the DEK. - All IndexedDB reads pass through
decryptRecord()which decrypts the fields transparently. - On lock (manual or inactivity),
sodium.memzero()wipes the DEK from memory.
- The web application is built using Vite (
pnpm build). pnpm tauri buildis executed.- The
tauri-actionGitHub Action orchestrates the entire build, signing, and packaging process for all target platforms (macOS, Windows, Linux, Android, iOS).
pnpm releasebumps the version acrosspackage.json,tauri.conf.json, andCargo.toml, tags (e.g.,v0.7.0), and pushes.- The
release.ymlorchestrator workflow is triggered by thev*tag. - It creates a draft GitHub Release, then calls
release-desktop.ymlviaworkflow_callto build desktop binaries for all platforms. - The workflows build, sign, and (on macOS) notarize all application binaries.
- The compiled artifacts are uploaded to the draft GitHub Release.
- The
chore(release):commit pushed tomaintriggersci.yml, which detects the release commit and deploys to Cloudflare R2. - Once the draft is published,
deploy.ymlalso deploys the web application as a safety net. - The
newReleaseWebSocket event is broadcast to all connected clients, and the Tauri desktop app's auto-updater will detect and download the new version.
For the full release process, see RELEASES.md.
- DEVELOPMENT.md — Development guide for desktop and mobile
- RELEASES.md — Full release process documentation
- SECURITY.md — Security hardening and code signing details
- SECRETS.md — Required secrets for CI/CD
- WEBSOCKET.md — WebSocket protocol and event routing
- PUSH_NOTIFICATIONS.md — Push notification architecture
- TAURI_TESTING.md — Testing strategy for Tauri apps