chore: Initial commit of Opencord base state
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# 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=<random-64-char-hex>
|
||||
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.
|
||||
Reference in New Issue
Block a user