# CLAUDE.md — Opencord Brain File ## IDENTITY You are the sole developer of Opencord, an open-source, self-hosted Discord alternative. You are an expert full-stack TypeScript architect. You write production-quality code. You never cut corners. You never use placeholders. You finish what you start. ## MISSION Build Opencord from scratch as a complete, working application. All 8 phases. Every file fully implemented. No stubs, no TODOs, no "add later" comments. When you are done, a user must be able to docker compose up and have a fully working Discord clone. ## CRITICAL RULES - NEVER use placeholder code, TODO comments, or // ...rest of code shortcuts. Every function you write must be FULLY implemented with real logic. - NEVER use // ...rest of code or // similar to above shortcuts. Write out every single line. - NEVER skip files or say "you can add this later". Build everything NOW. - NEVER generate partial components. Every React component must be complete with all state, handlers, styling, and edge cases. - If you hit the output limit, STOP mid-sentence and continue EXACTLY where you left off in your next message. Do NOT summarize or skip ahead. - Write production-quality code from the start. Proper error handling, input validation, TypeScript strict mode, no any types. - After completing each phase, UPDATE the ## PROGRESS section in CLAUDE.md marking it ✅ and noting what files were created. - After completing each phase, RUN the test commands listed to verify it works before moving on. - If something fails, FIX IT before moving on. Never leave broken code behind. ## TECH STACK (DO NOT DEVIATE) | Layer | Technology | Package | |-------|-----------|---------| | Runtime | Node.js 20+ with TypeScript (strict mode) | typescript, tsx | | Backend Framework | Fastify | fastify, @fastify/cors, @fastify/multipart, @fastify/static, @fastify/websocket | | Database | SQLite | better-sqlite3 | | ORM | Drizzle ORM | drizzle-orm, drizzle-kit | | Auth | JWT + bcrypt | jsonwebtoken, bcryptjs | | WebSocket | ws (via @fastify/websocket) | @fastify/websocket | | Frontend | React 18 + TypeScript | react, react-dom, react-router-dom | | Styling | Tailwind CSS 3 | tailwindcss, postcss, autoprefixer | | Build Tool | Vite | vite, @vitejs/plugin-react | | State Management | Zustand | zustand | | Voice/Video | LiveKit Client SDK | livekit-client, @livekit/components-react | | LiveKit Token | livekit-server-sdk | livekit-server-sdk | | Desktop | Electron | electron, electron-builder | | Monorepo | pnpm workspaces | pnpm | | Markdown | react-markdown | react-markdown | ## PROJECT STRUCTURE ``` Opencord/ ├── CLAUDE.md ├── package.json ├── pnpm-workspace.yaml ├── tsconfig.base.json ├── Dockerfile ├── docker-compose.yml ├── .env.example ├── .gitignore ├── README.md ├── packages/ │ ├── shared/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ │ │ └── types.ts │ ├── server/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ │ │ ├── index.ts │ │ ├── config.ts │ │ ├── db/ │ │ │ ├── schema.ts │ │ │ ├── index.ts │ │ │ └── seed.ts │ │ ├── routes/ │ │ │ ├── auth.ts │ │ │ ├── users.ts │ │ │ ├── servers.ts │ │ │ ├── channels.ts │ │ │ ├── messages.ts │ │ │ ├── uploads.ts │ │ │ ├── dm.ts │ │ │ └── livekit.ts │ │ ├── ws/ │ │ │ ├── handler.ts │ │ │ └── events.ts │ │ └── utils/ │ │ ├── auth.ts │ │ ├── snowflake.ts │ │ └── permissions.ts │ ├── web/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── vite.config.ts │ │ ├── tailwind.config.js │ │ ├── postcss.config.js │ │ ├── index.html │ │ └── src/ │ │ ├── main.tsx │ │ ├── App.tsx │ │ ├── api/ │ │ │ └── client.ts │ │ ├── stores/ │ │ │ ├── authStore.ts │ │ │ ├── serverStore.ts │ │ │ ├── chatStore.ts │ │ │ ├── voiceStore.ts │ │ │ └── uiStore.ts │ │ ├── hooks/ │ │ │ ├── useWebSocket.ts │ │ │ ├── useLiveKit.ts │ │ │ └── useAuth.ts │ │ ├── components/ │ │ │ ├── layout/ │ │ │ │ ├── AppLayout.tsx │ │ │ │ ├── ServerSidebar.tsx │ │ │ │ ├── ChannelSidebar.tsx │ │ │ │ ├── MainContent.tsx │ │ │ │ ├── MemberSidebar.tsx │ │ │ │ └── MobileNav.tsx │ │ │ ├── chat/ │ │ │ │ ├── MessageList.tsx │ │ │ │ ├── Message.tsx │ │ │ │ ├── MessageInput.tsx │ │ │ │ ├── TypingIndicator.tsx │ │ │ │ └── ImagePreview.tsx │ │ │ ├── voice/ │ │ │ │ ├── VoiceChannel.tsx │ │ │ │ ├── VoiceControls.tsx │ │ │ │ ├── VoiceGrid.tsx │ │ │ │ └── VoiceUser.tsx │ │ │ ├── auth/ │ │ │ │ ├── LoginPage.tsx │ │ │ │ └── RegisterPage.tsx │ │ │ ├── modals/ │ │ │ │ ├── CreateServer.tsx │ │ │ │ ├── InviteModal.tsx │ │ │ │ ├── CreateChannel.tsx │ │ │ │ ├── JoinServer.tsx │ │ │ │ ├── UserSettings.tsx │ │ │ │ └── ServerSettings.tsx │ │ │ └── ui/ │ │ │ ├── Avatar.tsx │ │ │ ├── Modal.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── ContextMenu.tsx │ │ │ └── LoadingSpinner.tsx │ │ └── styles/ │ │ └── globals.css │ └── desktop/ │ ├── package.json │ ├── tsconfig.json │ ├── src/ │ │ ├── main.ts │ │ └── preload.ts │ └── electron-builder.yml └── data/ ├── opencord.db └── uploads/ ``` ## DATABASE SCHEMA ```sql -- Users CREATE TABLE users ( id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, display_name TEXT, password_hash TEXT NOT NULL, avatar TEXT, status TEXT DEFAULT 'offline', custom_status TEXT, created_at INTEGER NOT NULL ); -- Servers (Guilds) CREATE TABLE servers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, icon TEXT, owner_id TEXT NOT NULL REFERENCES users(id), invite_code TEXT UNIQUE, created_at INTEGER NOT NULL ); -- Server Members CREATE TABLE server_members ( server_id TEXT REFERENCES servers(id) ON DELETE CASCADE, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, role TEXT DEFAULT 'member', nickname TEXT, joined_at INTEGER NOT NULL, PRIMARY KEY (server_id, user_id) ); -- Channels CREATE TABLE channels ( id TEXT PRIMARY KEY, server_id TEXT REFERENCES servers(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT NOT NULL, topic TEXT, position INTEGER DEFAULT 0, created_at INTEGER NOT NULL ); -- Messages CREATE TABLE messages ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), content TEXT, edited_at INTEGER, created_at INTEGER NOT NULL ); -- Attachments CREATE TABLE attachments ( id TEXT PRIMARY KEY, message_id TEXT REFERENCES messages(id) ON DELETE CASCADE, filename TEXT NOT NULL, original_name TEXT NOT NULL, mimetype TEXT NOT NULL, size INTEGER NOT NULL, created_at INTEGER NOT NULL ); -- DM Channels CREATE TABLE dm_channels ( id TEXT PRIMARY KEY, created_at INTEGER NOT NULL ); CREATE TABLE dm_members ( dm_channel_id TEXT REFERENCES dm_channels(id) ON DELETE CASCADE, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY (dm_channel_id, user_id) ); -- DM Messages CREATE TABLE dm_messages ( id TEXT PRIMARY KEY, dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), content TEXT, created_at INTEGER NOT NULL ); ``` ## REST API ENDPOINTS ``` POST /api/auth/register { username, password, displayName? } → { token, user } POST /api/auth/login { username, password } → { token, user } GET /api/users/@me (auth) → { user } PATCH /api/users/@me (auth) { displayName?, avatar?, customStatus? } → { user } GET /api/users/:id (auth) → { user } POST /api/servers (auth) { name, icon? } → { server } GET /api/servers (auth) → { servers[] } GET /api/servers/:id (auth) → { server, channels[], members[] } PATCH /api/servers/:id (auth, owner) { name?, icon? } → { server } DELETE /api/servers/:id (auth, owner) → { success } POST /api/servers/:id/join (auth) { inviteCode } → { server } POST /api/servers/:id/invite (auth, admin+) → { inviteCode } GET /api/servers/:id/channels (auth, member) → { channels[] } POST /api/servers/:id/channels (auth, admin+) { name, type, topic? } → { channel } PATCH /api/channels/:id (auth, admin+) { name?, topic?, position? } → { channel } DELETE /api/channels/:id (auth, admin+) → { success } GET /api/channels/:id/messages (auth, member) ?before=&limit=50 → { messages[] } POST /api/channels/:id/messages (auth, member) { content, attachments? } → { message } PATCH /api/messages/:id (auth, author) { content } → { message } DELETE /api/messages/:id (auth, author|admin) → { success } GET /api/servers/:id/members (auth, member) → { members[] } PATCH /api/servers/:id/members/:uid (auth, owner) { role } → { member } DELETE /api/servers/:id/members/:uid (auth, owner|self) → { success } POST /api/livekit/token (auth) { channelId } → { token } POST /api/uploads (auth) multipart file → { attachment } GET /api/uploads/:filename (public) → file stream GET /api/dm (auth) → { dmChannels[] } POST /api/dm (auth) { userId } → { dmChannel } GET /api/dm/:id/messages (auth, member) ?before=&limit=50 → { messages[] } POST /api/dm/:id/messages (auth, member) { content } → { message } ``` ## WEBSOCKET PROTOCOL All WebSocket messages are JSON. Client authenticates by sending `{ type: 'auth', token: 'jwt...' }` as first message. Server responds with `{ type: 'ready', user, servers, dmChannels }`. ### Client → Server ``` { type: 'auth', token: string } { type: 'message_create', channelId: string, content: string } { type: 'message_edit', messageId: string, content: string } { type: 'message_delete', messageId: string } { type: 'typing_start', channelId: string } { type: 'presence_update', status: 'online' | 'idle' | 'dnd' } { type: 'voice_join', channelId: string } { type: 'voice_leave' } { type: 'dm_message_create', dmChannelId: string, content: string } ``` ### Server → Client ``` { type: 'ready', user: User, servers: ServerWithChannelsAndMembers[], dmChannels: DmChannel[] } { type: 'message_created', message: MessageWithUser } { type: 'message_updated', message: MessageWithUser } { type: 'message_deleted', messageId: string, channelId: string } { type: 'typing', channelId: string, userId: string, username: string } { type: 'presence_update', userId: string, status: string } { type: 'voice_state_update', channelId: string, userId: string, action: 'join' | 'leave' } { type: 'member_joined', serverId: string, member: MemberWithUser } { type: 'member_left', serverId: string, userId: string } { type: 'dm_message_created', message: DmMessageWithUser } ``` ## LIVEKIT CONFIGURATION ``` LiveKit URL: wss://nova.ddns.net/livekit API Key: REDACTED_LIVEKIT_KEY API Secret: REDACTED_LIVEKIT_SECRET ``` ## ENVIRONMENT VARIABLES ``` PORT=3000 HOST=0.0.0.0 JWT_SECRET= LIVEKIT_URL=wss://nova.ddns.net/livekit LIVEKIT_API_KEY=REDACTED_LIVEKIT_KEY LIVEKIT_API_SECRET=REDACTED_LIVEKIT_SECRET UPLOAD_DIR=./data/uploads DB_PATH=./data/opencord.db MAX_UPLOAD_SIZE=104857600 REGISTRATION_OPEN=true ``` ## PROGRESS - Phase 1: Foundation — ✅ Complete (package.json, pnpm-workspace.yaml, tsconfig.base.json, packages/shared/src/types.ts, packages/server/src/index.ts, config.ts, db/schema.ts, db/index.ts, db/seed.ts, routes/auth.ts, routes/users.ts, utils/auth.ts, utils/snowflake.ts, utils/permissions.ts, .env) - Phase 2: Servers & Channels — ✅ Complete (routes/servers.ts, routes/channels.ts, updated index.ts) - Phase 3: Real-time Messaging — ✅ Complete (ws/handler.ts, ws/events.ts, routes/messages.ts, updated index.ts with @fastify/websocket) - Phase 4: Frontend — ✅ Complete (packages/web/ with all components, stores, hooks, API client, Vite config, Tailwind, Discord dark theme) - Phase 5: Voice/Video — ✅ Complete (routes/livekit.ts, voice components in frontend, LiveKit hooks) - Phase 6: File Sharing & DMs — ✅ Complete (routes/uploads.ts, routes/dm.ts, @fastify/multipart, join-by-invite endpoint) - Phase 7: Electron — ✅ Complete (packages/desktop/src/main.ts with IPC handlers, preload.ts, electron-builder.yml, tsconfig.json) - Phase 8: Docker — ✅ Complete (Dockerfile multi-stage build, docker-compose.yml, .env.example, .gitignore, .dockerignore, README.md) ## CURRENT PHASE All 8 phases complete.