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
