A private, encrypted, real-time desktop chat — built on Tauri, React, and Convex.
📦 Install · ✨ Features · 🚀 Self Host · 🔨 Build · 🗂️ Structure · 🌐 Website · 💬 Discord ❤️ Support
- Overview
- Installation
- Features
- End-to-End Encryption
- Real-time Messaging
- Media Sharing
- Message Management
- Privacy Controls
- Privacy by Default — Zero-Knowledge Session Model
- App Lock
- Disappearing Messages
- Chat Themes
- Notifications
- System Tray
- Auto Updater
- In-Chat Search
- Starred Messages
- Friend System
- Pinned Messages
- Message Reactions
- Tech Stack
- Self Hosting
- Building from Source
- Project Structure
- Convex Schema
- Cryptography
- Contributing
- Roadmap
- License
Lunex is a native desktop chat application built with privacy and security as first principles. Every message, every media file, every emoji reaction is encrypted end-to-end using NaCl box cryptography before it ever touches the server. Convex powers the real-time backend — but Convex itself never sees plaintext. Your data belongs to you.
Lunex is built on Tauri v2 (Rust + WebView), meaning it ships as a lean native binary not an Electron app bundled with a browser. The result is a fast, low-memory, native-feeling chat application for Windows and Linux.
Via winget (recommended):
winget install Lunex.LunexVia installer:
Download the latest .msi or .exe from the Releases page and run it.
Download the package that matches your distribution from the Releases page:
| Package | Distro |
|---|---|
.AppImage |
Universal — works on any Linux distro |
.deb |
Ubuntu, Debian, Linux Mint |
.rpm |
Fedora, openSUSE, RHEL |
AppImage (universal):
chmod +x Lunex_*.AppImage
./Lunex_*.AppImageDebian / Ubuntu:
sudo dpkg -i lunex_*.debFedora / openSUSE:
sudo rpm -i lunex_*.rpmEvery piece of data that leaves your device is encrypted before transmission. Lunex uses NaCl box (TweetNaCl) — the same cryptographic primitive used by Signal and WhatsApp for all message encryption.
How it works:
- On signup, each user generates an asymmetric key pair (public key + private key) derived from a BIP-39 mnemonic phrase (12 words)
- The private key (
secretKey) never leaves your device and is never sent to Convex - Messages are encrypted using NaCl box with the sender's private key and the recipient's public key this is Diffie-Hellman key exchange baked in
- Media files are encrypted with AES-GCM using a per-file random IV before upload
- Emoji reactions are individually encrypted before being stored
- Even the last message preview in the chat list is encrypted the server only ever stores ciphertext
Key files:
src/crypto/encryption.ts— NaCl box encrypt/decrypt + AES-GCM symmetric encrypt/decryptsrc/crypto/keyDerivation.ts— base64 encode/decode for key materialsrc/crypto/mediaEncryption.ts— media file AES-GCM encryption/decryptionsrc/crypto/mnemonic.ts— BIP-39 mnemonic generation and parsingsrc/crypto/pinEncryption.ts— PIN-based AES-GCM encryption for App Locksrc/crypto/dpEncryption.ts— display picture (profile photo) encryption
Powered by Convex reactive queries. When a message is sent, all participants see it instantly via WebSocket subscription no polling required.
- Messages load with the latest 30 messages on chat open (paginated scroll to top loads older messages)
- Real-time typing indicators — see when the other person is typing
- Read receipts — Empty circle (sent), circle with one tick (delivered), filled circle with one tick (seen)
- Delivery receipts — messages are marked delivered when the recipient's app receives them
- Online/offline status is updated on app open, app close, system shutdown, and window hide
Send images, videos, and documents all encrypted before upload.
- Files are encrypted client-side with a random AES-GCM IV before being sent to Convex storage
- The encrypted blob and its IV are stored; only the recipient (who holds the correct private key) can decrypt
- Media expiry — media files on Convex are automatically deleted after 6 hours via a cron job
- Batch uploads — multiple files sent in one message are grouped by
uploadBatchIdand displayed as a media grid - Upload progress is tracked per-conversation in a pending uploads list shown above the input bar
- Supported types:
image,video,file - Media is decrypted in memory only when displayed never written to disk as plaintext
Key files:
src/hooks/useMediaUpload.ts— file selection, encryption, upload, progress trackingsrc/components/chat/media/PendingUploadsList.tsx— in-progress upload displayconvex/media.ts— Convex storage URL generationconvex/cleanup.ts— media expiry deletion functionconvex/crons.ts— scheduled cleanup every 6 hours
- Edit messages — edit your own sent messages; edited messages are marked with an "edited" label
- Delete for me — remove a message from your view only
- Delete for everyone — remove a message from both sides (hard delete on Convex)
- Bulk delete — enter select mode via context menu, select multiple messages, delete all at once
- Reply to messages — reply to any specific message; reply preview shows in the input bar
- Message info — see exact sent, delivery and read timestamps per recipient
- Context menu — right-click anywhere in the chat area for quick actions (select messages, close chat)
Key files:
src/components/chat/area/ChatAreaDeleteDialog.tsx— delete confirmation dialog (for me / for everyone)src/components/chat/area/ChatAreaContextMenu.tsx— right-click context menusrc/components/chat/misc/MessageInfoPanel.tsx— delivery/read info panelsrc/hooks/useMessageSelection.ts— bulk selection state and delete handlersconvex/messages.ts— all message mutations (send, edit, delete, react, star, pin)
Each user has granular privacy settings for four attributes, each independently configurable to:
- Everyone — visible to all users
- Nobody — hidden from all
- Only these — allowlist of specific contacts
- All except — blocklist of specific contacts
Controllable attributes:
- Online status — whether others see you as online or offline
- Typing indicator — whether others see "typing..."
- Read receipts — whether others see filled circle with one tick
- Message notifications — whether you appear as a notification sender
Block list:
- Block any user — they can no longer send you friend requests or messages
- Blocked users are listed in your profile panel and can be unblocked at any time
Key files:
src/components/sidebar/settings/SettingsPrivacySection.tsx— privacy settings UIsrc/components/sidebar/settings/PrivacySelectorModal.tsx— everyone/nobody/only-these/all-except pickersrc/components/sidebar/settings/ContactPicker.tsx— contact picker for exception listsconvex/users.ts— privacy field reads with exception enforcementconvex/friends.ts— block/unblock, friend request mutations
Lunex is designed so that no sensitive data ever touches persistent storage by default. The privacy model has two distinct tiers, and you choose which one fits your needs.
This is the default behaviour when App Lock is disabled.
Every time you open Lunex and log in with your 12-word mnemonic phrase, your private key is derived and held exclusively in RAM for that session. The moment you close the app, every trace of your identity your private key, your decrypted messages, your session state is gone. Nothing is written to disk. Nothing persists.
App opens → mnemonic entered → secretKey derived in RAM → session active
App closes → RAM cleared → secretKey gone → no trace left on device
What this means in practice:
- Every new session requires your 12-word phrase no shortcuts, no remembered state
- A forensic examination of your device's storage after closing the app finds nothing belonging to Lunex
- If someone steals your laptop while the app is closed, there is nothing to extract
- The system tray toggle interacts with this model: if you enable system tray, closing the window keeps the app running in the background (RAM still holds your session, app stays usable). If you disable system tray, closing the window terminates the process and wipes RAM full privacy on every close
If re-entering 12 words on every launch is inconvenient, you can opt into App Lock in Settings. This enables a 6-digit PIN that persists your session across app restarts without compromising your private key security.
Here is exactly what happens when you enable App Lock:
User enables App Lock → sets 6-digit PIN
└→ secretKey (from RAM) is encrypted with AES-GCM using PIN as key source
└→ encrypted key blob stored in Tauri plugin-store (OS-level secure storage)
└→ RAM cleared of raw secretKey
App restarts → PIN lock screen shown
└→ user enters 6-digit PIN
└→ AES-GCM decrypt → secretKey recovered → loaded into RAM
└→ session resumes — no mnemonic needed
What this means in practice:
- Your raw private key is never stored in plaintext — only as an AES-GCM encrypted blob
- The PIN itself is never stored anywhere — it is only used transiently as key material during decrypt
- Without the correct PIN, the encrypted blob is cryptographically useless
- App Lock also hides your profile picture and bio on the lock screen — the lock screen itself reveals nothing about whose app this is
- Auto-lock timers (1 min · 5 min · 30 min · 1 hr) re-engage the PIN screen after inactivity
- Upon logout, the user’s PIN is permanently cleared and the encrypted key material stored on the system is securely deleted.
| Tier 1 (Default) | Tier 2 (App Lock) | |
|---|---|---|
| Login required every launch | Yes — 12-word phrase | No — 6-digit PIN |
| Private key on disk | Never | AES-GCM encrypted only |
| Data after app close | Zero | Encrypted key blob only |
| Best for | Maximum privacy | Daily convenience |
Key files:
src/store/authStore.ts— secretKey lives here in RAM only (never persisted without App Lock)src/crypto/pinEncryption.ts— AES-GCM encrypt/decrypt of secretKey with PINsrc/store/appLockStore.ts— App Lock enable state and auto-lock timersrc-tauri/src/lib.rs— system tray toggle that controls whether close = exit or close = minimize
Protect your Lunex session with a 6-digit PIN. When App Lock is enabled:
- A PIN lock screen covers the entire app on startup and after the auto-lock timer fires
- The mnemonic phrase is encrypted with AES-GCM using the PIN as the key source — the PIN itself is never stored anywhere
- Auto-lock timers: 1 minute, 5 minutes, 30 minutes, or 1 hour of inactivity
- Profile picture and bio are hidden on the lock screen (zero information leakage)
- Incorrect PIN entries show a shake animation with attempt feedback
Key files:
src/components/sidebar/settings/AppLockPanel.tsx— App Lock settings (enable/disable, change PIN, timer)src/components/sidebar/settings/AppLockPinPad.tsx— 6-digit PIN pad componentsrc/components/sidebar/settings/AppLockTimerSection.tsx— auto-lock timer radio selectorsrc/components/auth/PinLockScreen.tsx— full-screen PIN entry overlaysrc/store/appLockStore.ts—isAppLockEnabled,isLocked,autoLockTimerstatesrc/crypto/pinEncryption.ts— AES-GCM mnemonic encryption/decryption with PIN
Both participants in a conversation can enable disappearing messages.
Available timers: 1 hour · 6 hours · 12 hours · 1 day · 3 days · 7 days
When a timer is active:
- New messages sent after enabling automatically have a
disappearsAttimestamp - A Convex cron job runs periodically to hard-delete expired messages from the database
- The chat header shows a timer indicator when disappearing mode is active
- Either participant can change or disable the timer; changes are logged as a system message
Global default — users can set a default disappearing timer in Settings that applies to all new conversations automatically.
Key files:
src/components/chat/misc/DisappearingPicker.tsx— timer picker panelsrc/components/sidebar/settings/SettingsTimerSection.tsx— global default timer settingconvex/conversations.ts—setDisappearingModemutationconvex/cleanup.ts— expired message deletionconvex/crons.ts— scheduled cleanup job
Per-conversation color customization. Each chat can have its own visual theme, independent of the global app theme.
Customizable elements:
- My message bubble color
- Other person's bubble color
- My message text color
- Other person's text color
- Chat background color
- Named preset themes
Themes are stored in Convex and sync across sessions automatically.
Key files:
src/components/chat/misc/ChatThemeCustomizer.tsx— full theme editor (color pickers, preset grid)src/hooks/useChatTheme.ts— applies per-chat theme CSS variables to the chat areasrc/store/themeStore.ts— global app theme state (light/dark/system) + chat presetsconvex/chatThemes.ts—getChatThemequery,setChatThememutation
Native desktop notifications via Tauri's notification plugin.
- New message notifications fire when the app is in the background or when a different chat is open
- Notification content respects privacy settings — if the sender has disabled notification privacy, no notification is shown for their messages
- Notifications work on both Windows and Linux
- Clicking a notification brings the app window to focus
Key files:
src/hooks/useAppNotifications.ts— subscribes to incoming messages and fires native OS notifications
Lunex minimizes to the system tray instead of closing — your chats stay connected in the background.
- Left-click the tray icon to show the window
- Right-click for a context menu:
Show/Hide WindowandExit Exitfires asystem-shutdownevent that sets your status to offline before quitting cleanly- Tray icon toggle — Turn ON or OFF System tray
- The
toggle_trayTauri command controls tray icon visibility from the frontend
Key files:
src-tauri/src/lib.rs— tray setup, menu items, click handlers, close-to-tray window eventsrc/pages/ChatPage.tsx— listens forsystem-shutdownto set offline before quit
Lunex checks for updates automatically using Tauri's updater plugin.
- All updates are cryptographically signed with a private key — only official builds can be installed
updater.jsonin the repo root describes the latest version, download URLs, and per-platform signatures- On startup, Lunex checks the updater endpoint; if a newer version is available, the user is prompted to install and restart
- Three versions have been released and the update chain is live and tested
Key files:
updater.json— update manifest (version, platforms, download URLs, signatures)src-tauri/src/lib.rs—tauri_plugin_updaterregistrationsrc-tauri/Cargo.toml—tauri-plugin-updater = "2"dependency
Search through messages within any open conversation directly from the chat header.
- Click the Search icon in the chat header to open the search panel (slides in from the right)
- Type any query — results filter in real-time from the currently loaded decrypted messages
- Each result shows the sender name, timestamp, and the matched text highlighted in context
- Click any result to jump to that message — it scrolls into view and briefly highlights
- The search panel is a full sidebar panel, separate from the profile/info panel
Note: Search currently covers messages loaded in the current session. Full-history search across all messages is planned for a future release.
Key files:
src/components/chat/misc/ChatSearchPanel.tsx— search UI, real-time filtering, jump-to-messagesrc/components/chat/misc/ChatHeader.tsx— Search icon button that opens the panelsrc/store/chatStore.ts—searchPanelOpenstate,currentDecryptedMessagesfor search
Star any message to save it for later reference. All starred messages are accessible from the sidebar.
- Use the message context menu to star or unstar a message
- The Starred Messages panel (accessible from the 3 Dots Menu) shows all starred messages across all conversations, sorted by time
- Each entry shows the conversation it came from, the sender, the timestamp, and the message content
- Starring state is stored in Convex on the
starredByarray of the message document
Key files:
src/components/sidebar/StarredMessagesPanel.tsx— starred messages list with jump-to-chatconvex/messages.ts—starMessage/unstarMessagemutations
Lunex uses a friend-request model — you must be friends with someone before you can open a conversation.
- Search users by username from the Requests Page Find tab
- Send a friend request — the recipient sees it in their Requests Page Received tab
- Accept or reject incoming requests
- Once accepted, a conversation is automatically created and appears in both users' chat lists
- Block any user from their profile panel or from the blocked list in Settings
- Blocked users cannot send friend requests to you and cannot message you
Key files:
src/components/friends/— friend request UI cardssrc/components/chat/list/ChatList.tsx— tabs: Chats / Requests / Searchconvex/friends.ts—sendFriendRequest,acceptFriendRequest,rejectFriendRequest,blockUser,unblockUserconvex/conversations.ts—createConversation(called automatically on friend accept)
Pin important messages in a conversation for quick reference.
- Use the message context menu to pin or unpin a message
- You can pin a maximum of 3 messages in one chat
- Pinned messages appear in a pinned bar at the top of the chat area, below the header
- If multiple messages are pinned, the bar cycles through them on each click with a counter indicator
- Clicking the pinned bar jumps the scroll position to that message
- Pinned message IDs are stored in the
conversations.pinnedMessagesarray on Convex
Key files:
src/components/chat/area/ChatAreaPinnedBar.tsx— pinned message bar with cycle navigationconvex/messages.ts—pinMessage/unpinMessagemutations
React to any message with any emoji.
- Hover on a message to open the emoji reaction bar
- Reactions are individually encrypted — each emoji is AES-GCM encrypted before being stored on Convex
- Each message shows a reaction summary (emoji + count) below the bubble
- Remove your own reaction by clicking it again
- The last reaction in a conversation is stored on the conversation document for quick display in the chat list
Key files:
src/components/chat/bubble/— message bubble with reaction display and picker triggerconvex/messages.ts—addReaction/removeReactionmutations with encrypted emoji storage
| Layer | Technology |
|---|---|
| Desktop runtime | Tauri v2 (Rust) |
| Frontend framework | React 19 + TypeScript |
| Build tool | Vite 7 |
| Backend / real-time DB | Convex |
| Styling | Tailwind CSS v4 |
| UI components | shadcn/ui + Radix UI |
| State management | Zustand v5 |
| Routing | React Router v7 |
| Cryptography | TweetNaCl + Web Crypto API (AES-GCM) |
| Mnemonic / key gen | @scure/bip39 + @noble/hashes |
| Icons | Lucide React |
| Toasts | Sonner |
| Emoji picker | emoji-picker-react |
| PIN input | input-otp |
| Date utilities | date-fns |
| Tauri plugins | shell, notification, updater, process, fs, dialog, opener, store |
You will need:
git clone https://github.com/miangee21/Lunex.git
cd Lunex
npm installIn Terminal 1, start the Convex dev server:
npx convex dev- You will be prompted to log in to Convex (browser opens)
- Select Create a new project, name it
lunexormy-chat-app - Convex automatically creates
.env.localin your project root with your dev deployment URL
In Terminal 2, start the Tauri dev build:
npx tauri devThe app window will open. Create accounts, add friends, and send messages — everything is fully functional in development mode.
-
Go to your Convex Dashboard
-
Open your project → click the Production tab
-
Under Settings, copy these three values:
- Production Deploy Key — looks like
prod:f... - Convex URL — looks like
https://f***.convex.cloud - Convex Site URL — looks like
https://f***.convex.site
- Production Deploy Key — looks like
-
Create
.env.productionin your project root:
VITE_CONVEX_URL="https://f*********************.convex.cloud"
VITE_CONVEX_SITE_URL="https://f*******************.convex.site"Tauri requires all distributed builds to be cryptographically signed. Generate your key pair:
npx tauri signer generateYou will be prompted to set a password — choose a strong one and save it somewhere safe. You will need it every time you produce a release build.
Tauri outputs a private key and a public key. Save both.
Create .env in your project root:
TAURI_SIGNING_PRIVATE_KEY="your_private_key_here"
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your_password_here"Important: Add
.envto your.gitignore. Never commit your signing private key to version control.
After completing setup you will have exactly 3 env files in your project root:
| File | Purpose | Created by |
|---|---|---|
.env.local |
Dev Convex deployment URL | Convex CLI (auto-generated in Step 2) |
.env.production |
Production Convex URLs | You (Step 4) |
.env |
Tauri signing keys | You (Step 5) |
- Node.js v20+
- Rust stable toolchain — run
rustup update stable - Linux only: install system libraries first:
# Ubuntu / Debian
sudo apt install libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
# Fedora
sudo dnf install webkit2gtk4.1-devel openssl-devel gtk3-devel libappindicator-gtk3-devel librsvg2-develOpen PowerShell in the project root. Run these commands in order:
# 1. Deploy Convex schema and functions to production
$env:CONVEX_DEPLOY_KEY="prod:f**********************************"
npx convex deploy
# 2. Set signing key environment variables
$env:TAURI_SIGNING_PRIVATE_KEY="your_private_key_here"
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your_password_here"
# 3. Build the app
npx tauri buildOutput files (inside src-tauri/target/release/bundle/):
| File | Description |
|---|---|
msi/Lunex_x.x.x_x64_en-US.msi |
Windows Installer package |
nsis/Lunex_x.x.x_x64-setup.exe |
NSIS installer executable |
The Convex production deployment only needs to be done once. If you already ran
npx convex deployon Windows, skip that step here.
Open a terminal in the project root:
# 1. Set signing key environment variables
export TAURI_SIGNING_PRIVATE_KEY="your_private_key_here"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your_password_here"
# 2. Build the app
npx tauri buildIf you encounter AppImage build errors, use this alternative command:
NO_STRIP=1 APPIMAGE_EXTRACT_AND_RUN=1 npm run tauri buildOutput files (inside src-tauri/target/release/bundle/):
| File | Description |
|---|---|
appimage/lunex_x.x.x_amd64.AppImage |
Universal Linux portable app |
deb/lunex_x.x.x_amd64.deb |
Debian / Ubuntu package |
rpm/lunex-x.x.x-1.x86_64.rpm |
Fedora / openSUSE / RHEL package |
Lunex/
├── .github/
│ └── assets/
│ └── app.png # App screenshot for README
├── convex/ # Convex backend (serverless functions + schema)
│ ├── schema.ts # Database schema (all tables and indexes)
│ ├── messages.ts # Message CRUD, reactions, starring, pinning
│ ├── conversations.ts # Conversation creation, disappearing mode
│ ├── users.ts # User queries, privacy, profile, online status
│ ├── friends.ts # Friend requests, blocking
│ ├── chatThemes.ts # Per-chat theme get/set
│ ├── media.ts # Convex storage URL generation
│ ├── typing.ts # Typing indicator set/clear
│ ├── presence.ts # Online/offline presence tracking
│ ├── cleanup.ts # Media expiry + disappearing message deletion
│ └── crons.ts # Scheduled jobs (cleanup every 6 hours)
├── src/ # React frontend
│ ├── main.tsx # App entry point (Convex provider setup)
│ ├── App.tsx # Root component (renders AppRouter)
│ ├── App.css # Global styles, custom scrollbar, theme tokens
│ ├── crypto/ # All cryptographic operations
│ │ ├── encryption.ts # NaCl box + AES-GCM symmetric encrypt/decrypt
│ │ ├── keyDerivation.ts # Base64 encode/decode for key material
│ │ ├── mediaEncryption.ts # Media file AES-GCM encryption/decryption
│ │ ├── mnemonic.ts # BIP-39 mnemonic generation and parsing
│ │ ├── pinEncryption.ts # PIN-based AES-GCM for App Lock
│ │ ├── dpEncryption.ts # Display picture (profile photo) encryption
│ │ └── index.ts # Re-exports
│ ├── store/ # Zustand global state stores
│ │ ├── authStore.ts # userId, username, publicKey, secretKey
│ │ ├── chatStore.ts # activeChat, sidebar views, panel states
│ │ ├── appLockStore.ts # isAppLockEnabled, isLocked, autoLockTimer
│ │ ├── themeStore.ts # Global app theme + chat presets
│ │ └── settingsStore.ts # Misc settings state
│ ├── hooks/ # Custom React hooks
│ │ ├── useChatData.ts # Fetches raw messages, handles pagination
│ │ ├── useDecryptMessages.ts # Decrypts raw messages into DecryptedMessage[]
│ │ ├── useChatScroll.ts # Scroll container, scroll-to-bottom, load-on-top
│ │ ├── useChatTheme.ts # Applies per-chat theme CSS variables
│ │ ├── useMessageSelection.ts # Bulk selection state and delete handlers
│ │ ├── useMediaUpload.ts # File encrypt, upload, and progress tracking
│ │ ├── useAppNotifications.ts # Native desktop notifications on new messages
│ │ ├── useOnlineStatus.ts # User presence (online/offline)
│ │ ├── useProfilePicUrl.ts # Fetches and decrypts profile picture URL
│ │ └── useSecureAvatar.ts # Secure avatar display with decryption
│ ├── pages/ # Top-level page components
│ │ ├── ChatPage.tsx # Main app layout (SlimBar + sidebar + chat area)
│ │ ├── LoginPage.tsx # Login with username + mnemonic phrase
│ │ ├── SignupPage.tsx # New account creation (key pair + mnemonic)
│ │ └── SplashPage.tsx # Initial loading splash screen
│ ├── routes/
│ │ └── AppRouter.tsx # Auth gate + App Lock PIN screen gate
│ ├── components/
│ │ ├── auth/
│ │ │ └── PinLockScreen.tsx # Full-screen PIN entry when app is locked
│ │ ├── chat/
│ │ │ ├── area/ # Main chat panel
│ │ │ │ ├── ChatArea.tsx # Chat area root component
│ │ │ │ ├── MessageList.tsx # Message list with media grid grouping
│ │ │ │ ├── ChatAreaPinnedBar.tsx # Pinned message bar with cycle navigation
│ │ │ │ ├── ChatAreaDeleteDialog.tsx # Bulk delete confirmation dialog
│ │ │ │ └── ChatAreaContextMenu.tsx # Right-click context menu
│ │ │ ├── bubble/ # Message bubble components
│ │ │ ├── input/ # Chat input bar
│ │ │ │ └── ChatInput.tsx # Input bar with send/emoji/attach/select mode
│ │ │ ├── list/ # Chat list sidebar
│ │ │ │ └── ChatList.tsx # Tabs: Chats / Requests / Search
│ │ │ ├── media/ # Media components
│ │ │ │ └── PendingUploadsList.tsx # In-progress uploads display
│ │ │ └── misc/ # Chat utility panels
│ │ │ ├── ChatHeader.tsx # Chat header (avatar, name, online, actions)
│ │ │ ├── ChatSearchPanel.tsx # In-chat message search panel
│ │ │ ├── ChatThemeCustomizer.tsx # Per-chat color theme editor
│ │ │ ├── DisappearingPicker.tsx # Disappearing message timer picker
│ │ │ ├── MessageInfoPanel.tsx # Delivery/read timestamps panel
│ │ │ └── MessageStatusTick.tsx # Sent/delivered/read tick component
│ │ ├── friends/ # Friend request UI components
│ │ ├── profile/ # Profile panels
│ │ │ ├── MyProfilePanel.tsx # Your own profile editor
│ │ │ └── OtherUserPanel.tsx # Other user's profile view
│ │ ├── sidebar/ # Sidebar components
│ │ │ ├── SlimBar.tsx # Narrow icon bar (leftmost column)
│ │ │ ├── AvatarMenu.tsx # Avatar click dropdown menu
│ │ │ ├── DotsMenu.tsx # Three-dot dropdown menu
│ │ │ ├── AboutPanel.tsx # About / version info panel
│ │ │ ├── StarredMessagesPanel.tsx # All starred messages list
│ │ │ └── settings/ # Settings sub-panels
│ │ │ ├── SettingsPanel.tsx # Main settings root
│ │ │ ├── SettingsPrivacySection.tsx # Privacy toggles
│ │ │ ├── SettingsTimerSection.tsx # Global disappearing timer
│ │ │ ├── AppLockPanel.tsx # App Lock settings
│ │ │ ├── AppLockPinPad.tsx # 6-digit PIN pad component
│ │ │ ├── AppLockTimerSection.tsx # Auto-lock timer selector
│ │ │ ├── PrivacySelectorModal.tsx # everyone/nobody/only-these picker
│ │ │ └── ContactPicker.tsx # Contact picker for exception lists
│ │ ├── shared/ # Shared utility components
│ │ │ └── LunexLogo.tsx # App logo component
│ │ └── ui/ # shadcn/ui primitives
│ ├── types/
│ │ └── chat.ts # DecryptedMessage, ActiveChat, and core types
│ └── lib/
│ └── utils.ts # cn() utility (tailwind-merge + clsx)
├── src-tauri/ # Tauri Rust backend
│ ├── src/
│ │ ├── main.rs # Binary entry point
│ │ └── lib.rs # App setup: tray, plugins, commands, window events
│ ├── capabilities/ # Tauri permission definitions
│ ├── icons/ # App icons (all sizes, all platforms)
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri config (bundle ID, window, updater)
├── public/ # Static assets
├── updater.json # Auto-update manifest
├── index.html # Vite HTML entry point
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
└── package.json # npm dependencies and scripts
All sensitive fields (message content, IVs, reactions) are stored as ciphertext — Convex never receives or stores plaintext.
| Table | Purpose | Key fields |
|---|---|---|
users |
User accounts and settings | username, publicKey, isOnline, lastSeen, privacy settings |
conversations |
Chat sessions between two users | participantIds, disappearingMode, pinnedMessages, lastMessageAt |
messages |
All messages | encryptedContent, iv, type, sentAt, readBy, deliveredTo, reactions, starredBy |
friendRequests |
Pending/accepted/rejected requests | fromUserId, toUserId, status |
blockedUsers |
Blocked user pairs | blockerId, blockedId |
typingIndicators |
Real-time typing state | conversationId, userId, isTyping, updatedAt |
chatDeletions |
"Delete for me" history | conversationId, userId, deletedAt |
chatThemes |
Per-chat color themes | userId, otherUserId, bubble colors, text colors, preset name |
Key indexes:
messages.by_conversationon[conversationId, sentAt]— efficient paginated message loadingmessages.by_expireson[mediaExpiresAt]— media cleanup cron targetmessages.by_disappearson[disappearsAt]— disappearing message cleanupusers.by_username— username searchfriendRequests.by_pair— duplicate request preventionchatDeletions.by_user_conversation— fast "delete for me" filtering per chat
Lunex's security model treats the Convex server as untrusted. Everything sensitive is encrypted before it leaves your device.
BIP-39 mnemonic (12 words)
└→ SHA-512 hash (via @noble/hashes)
└→ first 32 bytes → NaCl secretKey (private key)
└→ NaCl box.keyPair.fromSecretKey() → publicKey
The publicKey is uploaded to Convex. The secretKey is derived fresh from the mnemonic on each login and kept only in memory (authStore). The mnemonic is shown once at signup and never stored by the app unless App Lock is enabled — in which case it is AES-GCM encrypted with the PIN before storage.
sender secretKey + recipient publicKey
└→ NaCl box (Curve25519 DH + XSalsa20-Poly1305)
└→ { encryptedContent (base64), iv (base64 nonce) }
└→ stored in Convex messages table
file bytes
└→ crypto.getRandomValues(12 bytes) → IV
└→ AES-GCM encrypt (secretKey[:32] as key)
└→ encrypted blob → uploaded to Convex storage
└→ { mediaStorageId, mediaIv } → stored on message
user PIN
└→ AES-GCM key derivation
└→ AES-GCM encrypt(mnemonic phrase)
└→ stored in Tauri plugin-store (encrypted at rest)
→ on unlock: AES-GCM decrypt → mnemonic → re-derive secretKey
Contributions are welcome. To contribute:
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes
- Test with the dev build:
npx tauri dev - Commit with a descriptive message:
git commit -m "feat: add my feature" - Push and open a Pull Request
Code conventions:
- TypeScript strict mode — avoid
any - Components go in the appropriate subdirectory under
src/components/ - New Convex queries and mutations go in the relevant file in
convex/ - State goes in a Zustand store — no prop drilling for global state
- All user data passed to Convex must be encrypted client-side first
These features are actively planned and will be added in upcoming releases.
| Feature | Status | |
|---|---|---|
| ⬜ | Mobile App | 📋 Planned |
Status: Planned
A native mobile version of Lunex for Android and iOS, built on the same Tauri + React codebase.
Planned scope:
- Full feature parity with the desktop app — end-to-end encryption, disappearing messages, media sharing, app lock, and all privacy controls
- Same Convex backend — your account, contacts, and message history carry over seamlessly between desktop and mobile
- Native push notifications
- Biometric unlock (fingerprint / Face ID) as an alternative to PIN
MIT © 2026 Muhammad Hassan
