Private messaging app built with Tauri v2 (Rust backend + vanilla JS frontend) on the Nostr protocol. Supports desktop (macOS, Windows, Linux) and Android.
npm run dev # Desktop development (Tauri dev server)
npm run build # Desktop release build
npm run dev:bare # Dev without whisper feature (faster compile)
npm run build:bare # Release without whisper feature
npm run android:dev # Android dev (./scripts/android-dev.sh)
npm run android:build # Android release (tauri android build)Frontend build: node scripts/build-frontend.mjs copies src/ to dist/ with optional minification (terser + lightningcss in release).
Vector Core test suite: cd crates && cargo test -p vector-core.
All business logic lives here, fully decoupled from Tauri. Any client (GUI, CLI, SDK, bot) imports this crate.
macros.rs— log_info!, log_debug!, log_trace!, log_warn! (#[macro_export])types.rs— Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadataprofile/— Profile, ProfileFlags, SlimProfile (Box optimized, u16 interner handles)profile/sync.rs— ProfileSyncHandler trait, SyncPriority queue, load_profile, update_profile, update_status, block/unblock, nickname, background processor
chat.rs— Chat, ChatType, ChatMetadata, SerializableChatcompact.rs— CompactMessage (u64 ms timestamps), CompactMessageVec, NpubInterner, TinyVec, bitflagsstate.rs— ChatState, all globals (NOSTR_CLIENT, MY_SECRET_KEY, STATE, etc.), WrapperIdCache, processing gatecrypto/— GuardedKey vault, GuardedSigner, Argon2id, AES-GCM, ChaCha20, decrypt_data, extension_from_mime, sanitize_filename, resolve_unique_filename, format_bytes, mime_from_magic_bytes, mime_from_extension (full MIME map)db/— SQLite schema, 20 atomic migrations, connection pools, RAII guards, settings KVhex.rs— SIMD hex encode/decode (NEON ARM64, SSE2/AVX2 x86_64, scalar fallback)rumor.rs— process_rumor() inbound message parser, RumorEvent, 11 result variantsstored_event.rs— StoredEvent, StoredEventBuilder, event_kind constantssending.rs— SendCallback trait, SendConfig, send_dm/send_file_dm/send_rumor_dm, retry_send_gift_wrapblossom.rs— File upload with progress tracking, retry, server failoverinbox_relays.rs— NIP-17 kind 10050 relay resolution, stampede-protected cache, gift-wrap sendingnet.rs— SSRF protection, build_http_clientstats.rs— CacheStats, DeepSize trait for memory benchmarking (debug builds)traits.rs— EventEmitter trait (abstracts UI notification), ProgressReporter
src-tauri consumes vector-core via path = "../crates/vector-core". Types and globals are re-exported — same instances, shared memory.
lib.rs— App entry, plugin registration,invoke_handlerwith 150+ commandscommands/— Tauri command handlers (thin wrappers around vector-core logic)state/— Re-exports vector-core globals + local TAURI_APP + TauriEventEmitter (bridges emit_event to Tauri)macros.rs— log_error! only (toast + log file via TAURI_APP; log_info/debug/trace/warn in vector-core)rumor.rs— Thin wrapper: re-exports vector-core + parse_mls_imeta_attachments + process_rumor_with_mls + resolve_download_dirmessage/— Re-exports vector-core types + TauriSendCallback + file dedup logicservices/— Event handler, subscription handler, notificationsmls/— MLS group encryption via OpenMLS/MDK (not yet in vector-core)miniapps/— WebXDC-compatible mini apps (Tauri-specific: custom protocol, WebView, Iroh P2P)android/— JNI bindings, localhost media server, background syncsimd/— SIMD image, audio, URL, HTML operations (hex moved to vector-core)
main.js— Main application logic (~25k lines, bundled)js/— ES modules: chat-scroll, emoji, file-preview, marketplace, settings, voice, db, platforms/styles.css— All styles (~7k lines)index.html— Single-page app shell
Frontend communicates with backend via window.__TAURI__.core.invoke().
Every new #[tauri::command] requires THREE things:
- Permission TOML in
src-tauri/permissions/autogenerated/<command_name>.toml(createallow-anddeny-entries) "allow-<command-name-with-hyphens>"added tosrc-tauri/capabilities/default.json- Registration in the
invoke_handlermacro inlib.rs
Missing any = invoke() silently rejects with "Command X not allowed by ACL".
All DM sends (text + file) flow through vector-core's send_dm/send_file_dm/send_rumor_dm:
SendCallbacktrait — 7 lifecycle hooks (on_pending, on_sent, on_failed, on_upload_progress, on_upload_complete, on_attachment_preview, on_persist) with default no-opsSendConfig— per-call config: max_send_attempts, retry_delay, self_send, cancel_token. Presets:gui()(12 retries),headless()(3),default()(1)TauriSendCallback— emits to JS frontend + DB persistenceCliSendCallback— terminal output for sent/failed/progress- Text DMs:
message()short-circuits tovector_core::send_dmwithTauriSendCallback - File DMs: src-tauri handles dedup + upload, then calls
vector_core::send_rumor_dmfor gift-wrap + retry - MLS groups: stay in src-tauri (MDK dependency)
All profile operations (fetch, publish, block, nickname) flow through vector-core's profile::sync module:
ProfileSyncHandlertrait —on_profile_fetched(slim, avatar_url, banner_url)with default no-op. Covers DB persistence + image caching.TauriProfileSyncHandler— spawnsdb::set_profile+cache_profile_imagesEventEmittertrait — abstracts UI notification.TauriEventEmitterbridges toTAURI_APP.emit(), registered at startup.- Profile ops in vector-core:
load_profile,update_profile,update_status,block_user,unblock_user,set_nickname,get_blocked_users - Sync queue:
SyncPriority(Critical/High/Medium/Low),ProfileSyncQueue,start_profile_sync_processor - src-tauri profile commands are one-line delegates to vector-core
Global state lives in src-tauri/src/state/ and is re-exported at crate root:
TAURI_APP,NOSTR_CLIENT,MY_KEYS,MY_PUBLIC_KEY,STATESTATEholdsArc<Mutex<AppState>>with chats, profiles, settings- Multi-account: separate SQLite DB per account in
~/.local/share/io.vectorapp/data/<npub>/
All commands return Result<T, String>. Errors are string-formatted for frontend display.
- WebView
shouldInterceptRequestthreads have NO tokio runtime —Handle::current()will PANIC. Usetry_lock()with retry loops for STATE access from JNI threads. - Localhost media server (
android/media_server.rs) serves files becauseasset://doesn't support Range requests for audio/video. - rustls must use
ringprovider (notaws-lc-rs) — pkarr is patched insrc-tauri/patches/pkarr/.
message/compact.rs defines CompactMessage / CompactMessageVec — a memory-optimized format using Box<str>, u16 npub interning, and [u8; 32] IDs instead of hex strings. Messages are stored in compact form in memory and converted to full Message structs for frontend serialization.
Files are encrypted (NIP-96/Blossom), uploaded to media servers, and referenced via SHA-256 hash. The name field carries the original filename through the protocol. Downloads save with human-readable names + collision suffixes (-1, -2). Hash-based dedup prevents re-downloading identical content.
Key crates: nostr-sdk 0.44, tauri 2.10, tokio 1.49, rusqlite 0.32, openmls, iroh 0.96, iroh-gossip 0.96, aes-gcm, argon2, image 0.25
Local path deps: ../../mdk/crates/mdk-* (MDK media/encryption library)
wry fork: [patch.crates-io] in Cargo.toml points to local ../../wry for WKWebView background color fix.
- macOS: WKWebView white flash prevented via
drawsBackgroundKVC on config (wry fork). Metal GPU for Whisper. - Linux:
WEBKIT_DISABLE_DMABUF_RENDERER=1set for WebKitGTK compatibility. - Android: API 26+. Vulkan GPU disabled for Whisper (device freeze). OpenSSL vendored.
- Feature flag:
whisper(default) — enables OpenAI Whisper transcription. Use--no-default-featuresto skip.