**Update this file when making significant changes.**

See also: [CONVEX_RULES.md](./CONVEX_RULES.md) | [CONVEX_EXAMPLES.md](./CONVEX_EXAMPLES.md)

## Architecture

- **Monorepo**: npm workspaces (`packages/*`, `apps/*`)
- **Backend**: Convex (reactive database + serverless functions)
- **Frontend**: React + Vite, shared codebase in `packages/shared/`
- **Platforms**: Electron (`apps/electron/`), Web (`apps/web/`), Android via Capacitor (`apps/android/`)
- **Platform Abstraction**: `usePlatform()` hook provides crypto, session, settings, idle, links, screenCapture, windowControls, updates APIs
- **Auth**: Zero-knowledge custom auth via Convex mutations (getSalt, verifyUser, createUserWithProfile)
- **Real-time**: Convex reactive queries (`useQuery` auto-updates all connected clients)
- **Voice/Video**: LiveKit (token generation via Convex Node action)
- **E2E Encryption**: Platform-specific crypto (Electron: Node crypto via IPC, Web: Web Crypto API)
- **File Storage**: Convex built-in storage (`generateUploadUrl` + `getUrl`)

## Project Structure

```
Discord Clone/
├── convex/                        # Backend (Convex functions + schema)
├── packages/
│   ├── shared/                    # Shared React app (all components, pages, contexts, styles)
│   │   └── src/
│   │       ├── components/        # All UI components
│   │       ├── pages/             # Login, Register, Chat
│   │       ├── contexts/          # VoiceContext, ThemeContext, PresenceContext
│   │       ├── platform/          # PlatformProvider + usePlatform hook
│   │       ├── styles/            # themes.css
│   │       ├── utils/             # userPreferences.js, streamUtils.jsx
│   │       ├── assets/            # sounds, icons, emojis, fonts
│   │       ├── App.jsx            # Router + AuthGuard
│   │       └── index.css          # Global styles
│   └── platform-web/              # Web/Capacitor platform implementations
│       └── src/
│           ├── crypto.js          # Web Crypto API (RSA-OAEP, Ed25519, AES-256-GCM, scrypt)
│           ├── session.js         # localStorage session persistence
│           ├── settings.js        # localStorage settings
│           ├── idle.js            # Page Visibility API idle detection
│           └── index.js           # Bundles all web platform APIs
├── apps/
│   ├── electron/                  # Electron desktop app
│   │   ├── main.cjs               # Electron main process
│   │   ├── preload.cjs            # IPC bridge (window.* APIs)
│   │   ├── updater.cjs            # electron-updater integration
│   │   ├── splash.html            # Update splash screen
│   │   └── src/
│   │       ├── main.jsx           # Entry: PlatformProvider + HashRouter
│   │       └── platform/index.js  # Electron platform (delegates to window.* APIs)
│   ├── web/                       # Web browser app
│   │   └── src/
│   │       └── main.jsx           # Entry: PlatformProvider + BrowserRouter
│   └── android/                   # Capacitor Android wrapper
│       └── capacitor.config.ts    # Points webDir at ../web/dist
├── package.json                   # Root workspace config
├── .env.local                     # Convex + LiveKit + Tenor keys
└── CLAUDE.md
```

## Key Convex Files (convex/)

- `schema.ts` - Full schema: userProfiles (with avatarStorageId, aboutMe, customStatus, joinSoundStorageId), categories (name, position), channels (with categoryId, topic, position), messages, messageReactions, channelKeys, roles, userRoles, invites, dmParticipants, typingIndicators, voiceStates, channelReadState, serverSettings (serverName, afkChannelId, afkTimeout, iconStorageId)
- `auth.ts` - getSalt, verifyUser, createUserWithProfile, getPublicKeys (includes avatarUrl, aboutMe, customStatus, joinSoundUrl), updateProfile (includes joinSoundStorageId, removeJoinSound), updateStatus, getMyJoinSoundUrl
- `categories.ts` - list, create, rename, remove, reorder
- `channels.ts` - list, get, create (with categoryId/topic/position), rename, remove (cascade), updateTopic, moveChannel, reorderChannels
- `members.ts` - getChannelMembers (includes isHoist on roles, avatarUrl, aboutMe, customStatus)
- `channelKeys.ts` - uploadKeys, getKeysForUser
- `messages.ts` - list (with reactions + username), send, edit, pin, listPinned, remove (with manage_messages permission check)
- `reactions.ts` - add, remove
- `serverSettings.ts` - get (resolves iconUrl), update (manage_channels permission), updateName (manage_channels permission), updateIcon (manage_channels permission), clearAfkChannel (internal)
- `typing.ts` - startTyping, stopTyping, getTyping, cleanExpired (scheduled)
- `dms.ts` - openDM, listDMs
- `invites.ts` - create, use, revoke
- `roles.ts` - list, create, update, remove, listMembers, assign, unassign, getMyPermissions
- `voiceState.ts` - join, leave, updateState, getAll (includes joinSoundUrl per user), afkMove (self-move to AFK channel)
- `voice.ts` - getToken (Node action, livekit-server-sdk)
- `files.ts` - generateUploadUrl, getFileUrl
- `gifs.ts` - search, categories (Node actions, Tenor API)
- `readState.ts` - getReadState, markRead, getAllReadStates, getLatestMessageTimestamps (unread tracking)

## Shared Frontend (packages/shared/src/)

- `App.jsx` - Router + AuthGuard (uses `usePlatform().session`)
- `pages/Login.jsx` - Convex auth (uses `usePlatform().crypto` for key derivation)
- `pages/Register.jsx` - Convex auth (uses `usePlatform().crypto` for key generation)
- `pages/Chat.jsx` - useQuery for channels, categories, channelKeys, DMs
- `components/ChatArea.jsx` - Messages, typing, reactions via Convex queries/mutations
- `components/Sidebar.jsx` - Channel/category creation, key distribution, invites, drag-and-drop via @dnd-kit
- `contexts/VoiceContext.jsx` - Voice state via Convex + LiveKit + idle detection via `usePlatform().idle`
- `components/TitleBar.jsx` - Conditional: only renders if `platform.features.hasWindowControls`
- `components/UpdateBanner.jsx` - Conditional: only renders if `platform.features.hasNativeUpdates`
- `components/ScreenShareModal.jsx` - Uses `usePlatform().screenCapture`
- `components/MessageItem.jsx` - Message rendering, link opening via `usePlatform().links`
- `platform/PlatformProvider.jsx` - React context providing platform APIs via `usePlatform()` hook

## Platform Abstraction (usePlatform())

All platform-specific APIs are accessed via the `usePlatform()` hook:
- `crypto` - generateKeys, randomBytes, sha256, signMessage, verifySignature, deriveAuthKeys, encryptData, decryptData, decryptBatch, verifyBatch, publicEncrypt, privateDecrypt
- `session` - save, load, clear
- `settings` - get, set
- `idle` - getSystemIdleTime, onIdleStateChanged, removeIdleStateListener
- `links` - openExternal, fetchMetadata
- `screenCapture` - getScreenSources
- `windowControls` - minimize, maximize, close (Electron only, null on web)
- `updates` - checkUpdate (Electron only, null on web)
- `features` - hasWindowControls, hasScreenCapture, hasNativeUpdates

## Important Patterns

- Channel IDs use Convex `_id` (not `id`) - all references use `channel._id`
- Auth: client hashes DAK -> HAK before sending, server does string comparison
- First user bootstrap: createUserWithProfile creates Owner + @everyone roles
- Vite configs use `envDir: '../../'` to pick up root `.env.local`
- Convex queries are reactive - no need for manual refresh or socket listeners
- File uploads use Convex storage: `generateUploadUrl` -> POST blob -> `getFileUrl`
- Typing indicators use scheduled functions for TTL cleanup
- CSS uses Discord dark theme colors via `:root` variables (`--bg-primary: #313338`, `--bg-secondary: #2b2d31`, `--bg-tertiary: #1e1f22`)
- Sidebar width is 312px (72px server strip + 240px channel panel)
- Channels grouped by `categoryId` with collapsible headers and @dnd-kit drag-and-drop
- Members list groups by hoisted roles (isHoist) then Online/Offline
- Unread tracking: `channelReadState` table per user/channel. ChatArea shows red "NEW" divider, Sidebar shows white dot
- Server name from `serverSettings` singleton, editable via Server Settings (requires `manage_channels`)
- AFK voice channel: VoiceContext polls idle time, auto-moves idle users
- Custom join sounds: stored as `joinSoundStorageId` on `userProfiles`
- Server icon: `serverSettings` stores `iconStorageId`, resolved to `iconUrl`
- `userPreferences.js` `setUserPref` takes optional `settings` param for disk persistence via platform

## Environment Variables

In `.env.local` at project root:
- `CONVEX_DEPLOYMENT` - Convex deployment URL (set by `npx convex dev`)
- `VITE_CONVEX_URL` - Convex URL for frontend (set by `npx convex dev`)
- `VITE_LIVEKIT_URL` - LiveKit server URL
- `LIVEKIT_API_KEY` - LiveKit API key (used in Convex Node action)
- `LIVEKIT_API_SECRET` - LiveKit API secret (used in Convex Node action)
- `TENOR_API_KEY` - Tenor GIF API key (used in Convex Node action)

## Running the App

1. `npm install` (installs all workspaces)
2. `npx convex dev` (starts Convex backend, creates `.env.local`)
3. Electron: `npm run dev:electron`
4. Web: `npm run dev:web`
5. Android: `npm run build:web && cd apps/android && npx cap sync && npx cap open android`
