Files
DiscordClone/CLAUDE.md
Bryan1029384756 593eaba82e 1.1.3
2026-04-18 15:41:51 -05:00

11 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Update this file when making significant changes.

See also: CONVEX_RULES.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, notifications, 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)

Development Commands

# Install
npm install                    # Installs all workspaces

# Backend
npx convex dev                 # Start Convex dev server (creates .env.local)

# Frontend (run alongside backend)
npm run dev:web                # Web app at localhost:5173
npm run dev:electron           # Electron app (Vite + Electron concurrently)

# Production builds
npm run build:web              # Web production build -> apps/web/dist
npm run build:electron         # Electron build with electron-builder
npm run build:android          # Web build + Capacitor sync

# Android
cd apps/android && npx cap sync && npx cap open android

# Preview
cd apps/web && npx vite preview  # Preview web production build

No test framework or linter is configured in this project.

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/                   # Web Crypto API, localStorage session/settings, Page Visibility idle
├── apps/
│   ├── electron/                  # Electron desktop app (main.cjs, preload.cjs, updater.cjs)
│   │   └── src/main.jsx           # Entry: PlatformProvider + HashRouter
│   ├── web/                       # Web browser app (PWA enabled via VitePWA)
│   │   └── src/main.jsx           # Entry: PlatformProvider + BrowserRouter
│   └── android/                   # Capacitor Android wrapper
├── package.json                   # Root workspace config
├── .env.local                     # Convex + LiveKit + Klipy keys
└── CLAUDE.md

Vite & Import Aliases

All Vite configs use envDir: '../../' to pick up root .env.local.

Alias Resolves to
@discord-clone/shared packages/shared/src/
@discord-clone/platform-web packages/platform-web/src/
@shared packages/shared/src/

Convex imports from shared code use a relative path whose depth depends on the file location: ../../../../convex/_generated/api from packages/shared/src/<dir>/file.tsx (4 up), ../../../../../convex/_generated/api from packages/shared/src/<dir>/<subdir>/file.tsx (5 up — applies to components/layout/, components/channel/, etc.). Count: go up until you're at the repo root, then into convex/.

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)
  • notifications - show, setBadge, flashFrame, ensurePermission (Electron: native; Web: Notification API + Badging where available; null on Android for now)
  • 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
  • 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
  • Module-scope functions needing crypto accept it as parameter (e.g., encryptKeyForUsers(users, channelId, keyHex, crypto))
  • randomBytes(size) returns hex string on both platforms
  • Keys exchanged as PEM strings (SPKI public, PKCS8 private) for cross-platform interop
  • TitleBar/UpdateBanner render conditionally based on platform.features.*
  • MessageContent.tsx parses Discord-style markdown (bold, italic, underline, strike, code, codeblock, > blockquote, ||spoiler||) on render — raw text is stored; parsing happens after decrypt. Inline emoji/mention/URL/custom-emoji tokenization runs inside each text leaf
  • NotificationManager (mounted in AppLayout) watches readState.getLatestMessageTimestamps across all channels. On a new messageId when the window is unfocused and senderId !== self, it calls platform.notifications.show + flash + badge. Own sends and initial snapshot are suppressed. Focus auto-clears flash/badge
  • Electron update flow is check-only on launch (no auto-install). updater.cjs emits status events; platform.updates.{getStatus,downloadAndInstall,onStatusChanged} expose it. HeaderUpdateIcon (mounted in TitleBar) renders a green download icon for optional updates and a full-screen blocker for required ones. Mark a release required by starting its release notes with [REQUIRED]
  • Moderation: bans table blocks login (auth.verifyUser) and message send (messages.sendInternal). auditLog table is append-only; audit.logAudit(ctx, {...}) is the helper that mutations call (best-effort — never throws). Permission check: roles.hasPermission(ctx, userId, key) — treats isAdmin and the Owner role as superusers so new permission keys like ban_members work without a migration. Server Settings → Bans + Audit Log tabs (desktop + mobile)
  • Profile banner: userProfiles.bannerStorageId (optional), resolved to bannerUrl in auth.getPublicKeys. auth.updateProfileInternal takes bannerStorageId + removeBanner (the remove path also ctx.storage.deletes the blob). All four profile card surfaces (MemberProfilePopout, MemberProfileModal, MobileMemberProfileSheet, UserAreaProfilePopout) render the image when present, fall back to accent color when not
  • Voice messages: mic button in ChannelTextarea records via MediaRecorder (picks audio/webm;codecs=opus where supported), stages the resulting File through the existing attachment pipeline — no new backend. Receivers render it via the standard AttachmentAudio player. Filename convention: voice-message-{timestamp}.{webm|ogg|m4a}. Voice-recorded messages set isVoiceMessage: true + peaks: number[] + durationSec in the attachment metadata; EncryptedAttachment dispatches those to VoiceMessagePlayer (pill with play button + waveform) instead of the full audio card
  • Push-to-talk: voiceSettings.inputMode is 'voice-activity' (default) or 'push-to-talk'. Paired with the voice.pushToTalk keybind (marked pressAndHold: true). KeybindContext dispatches brycord:keybind:voice.pushToTalk:down / :up events — pressAndHold actions never preventDefault, so binding PTT to a letter still lets you type. VoiceContext reads the settings via the brycord:voice-settings-changed window event, listens for the PTT events, and routes them through a configurable release-delay timer before reconciling the LiveKit mic track. All mic-on/mic-off sources (user mute, deafen, server mute, PTT gate) converge on a single setMicrophoneEnabled effect

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)
  • KLIPY_API_KEY - Klipy GIF API customer id (used in convex/gifs.ts). Replaces the old TENOR_API_KEY after Tenor's shutdown — the legacy var name is still read as a fallback so existing deployments only need to swap the value.