---
name: chat-project-maintainer
description: Use this skill when working on this specific chat project to understand backend structure, database schema, API/socket contracts, frontend layout, friendship flows, and safe change boundaries.
---

# Chat Project Maintainer Skill

## Purpose
Use this skill when changing this repository so work remains consistent with the current architecture:
- Backend API + Socket.IO contracts
- MySQL schema + migration flow
- Auth + message idempotency guarantees
- Frontend Vue/Pinia/Tailwind structure
- Friendship system behavior

## Repository Structure
- `api/` Node.js + Express + MySQL + Socket.IO backend
- `client/` Vue 3 + Pinia + Tailwind frontend
- `deploy/` deployment templates

## Backend Architecture (`api/`)

### Entry and Boot
- `api/server.js`
  - Loads env
  - Runs DB initialization + migrations
  - Creates HTTP server + attaches Socket.IO
- `api/app.js`
  - Middlewares: helmet, morgan, cors, JSON/urlencoded parsers, cookie-parser, rate-limit
  - Static uploads served at `/uploads`
  - Routes mounted under `/api/*`

### Database Layer
- `api/config/db.js`
  - Creates DB if missing (`DB_NAME`)
  - Creates MySQL pool
  - Applies migrations from `api/migrations/*.sql` (sorted)

### Core Services
- `api/services/messageService.js`
  - Message creation with transaction
  - Idempotency by `client_message_id`
  - Fetch conversation messages with `statusMap`
- `api/services/messageStatusService.js`
  - Upsert `sent|delivered|read`
- `api/services/friendService.js`
  - Friendship request lifecycle
  - Pending/friends/search queries
- `api/services/socket.js`
  - JWT-authenticated socket connections
  - Presence + typing + chat message sync + friendship events
  - Optional Redis adapter with `REDIS_URL`

### Middlewares
- `api/middlewares/auth.js` JWT bearer auth
- `api/middlewares/rateAuth.js` auth endpoint limiter
- `api/middlewares/errorHandler.js` unified JSON error response

## Database Schema

### Tables
- `users`
- `conversations`
- `conversation_participants`
- `messages`
- `message_status`
- `refresh_tokens`
- `friendships`
- `migrations_log`

### Column Types (Detailed)

#### `users`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `full_name` | `VARCHAR(150)` NOT NULL |
| `mobile` | `VARCHAR(20)` NOT NULL (UNIQUE) |
| `email` | `VARCHAR(255)` NOT NULL (UNIQUE) |
| `gender` | `ENUM('male','female')` NOT NULL |
| `password` | `VARCHAR(255)` NOT NULL |
| `profile_image` | `VARCHAR(512)` NULL |
| `bio` | `TEXT` NULL |
| `last_seen` | `DATETIME` NULL |
| `is_online` | `TINYINT(1)` DEFAULT `0` |
| `created_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |
| `updated_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` |

#### `conversations`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `type` | `ENUM('private','group')` NOT NULL DEFAULT `'private'` |
| `created_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |

#### `conversation_participants`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `conversation_id` | `BIGINT UNSIGNED` NOT NULL |
| `user_id` | `BIGINT UNSIGNED` NOT NULL |
| `joined_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |

#### `messages`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `conversation_id` | `BIGINT UNSIGNED` NOT NULL |
| `sender_id` | `BIGINT UNSIGNED` NOT NULL |
| `message_type` | `ENUM('text','image','file')` DEFAULT `'text'` |
| `message_text` | `TEXT` NULL |
| `file_url` | `VARCHAR(1024)` NULL |
| `is_edited` | `TINYINT(1)` DEFAULT `0` |
| `is_deleted` | `TINYINT(1)` DEFAULT `0` |
| `created_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |
| `updated_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` |
| `client_message_id` | `VARCHAR(255)` NULL (UNIQUE with `conversation_id`) |

#### `message_status`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `message_id` | `BIGINT UNSIGNED` NOT NULL |
| `user_id` | `BIGINT UNSIGNED` NOT NULL |
| `status` | `ENUM('sent','delivered','read')` NOT NULL |
| `updated_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` |

#### `refresh_tokens`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `user_id` | `BIGINT UNSIGNED` NOT NULL |
| `token` | `VARCHAR(512)` NOT NULL |
| `expires_at` | `DATETIME` NOT NULL |
| `created_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |

#### `friendships`
| Column | Type |
|---|---|
| `id` | `BIGINT UNSIGNED` (PK, AUTO_INCREMENT) |
| `requester_id` | `BIGINT UNSIGNED` NOT NULL |
| `recipient_id` | `BIGINT UNSIGNED` NOT NULL |
| `status` | `ENUM('pending','accepted','rejected')` NOT NULL DEFAULT `'pending'` |
| `user_low` | `BIGINT UNSIGNED` GENERATED STORED (`LEAST(requester_id, recipient_id)`) |
| `user_high` | `BIGINT UNSIGNED` GENERATED STORED (`GREATEST(requester_id, recipient_id)`) |
| `created_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |
| `updated_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` |

#### `migrations_log`
| Column | Type |
|---|---|
| `id` | `INT` (PK, AUTO_INCREMENT) |
| `file_name` | `VARCHAR(255)` NOT NULL |
| `applied_at` | `DATETIME` DEFAULT `CURRENT_TIMESTAMP` |

### Migration Files
- `000_init_migrations.sql`
- `001_create_users.sql`
- `002_create_conversations.sql`
- `003_create_participants.sql`
- `004_create_messages.sql`
- `005_create_message_status.sql`
- `006_add_client_message_id.sql`
- `007_refresh_tokens.sql`
- `008_create_friendships.sql`

## API Surface

### Health
- `GET /api/health`

### Auth
- `POST /api/auth/register`
- `POST /api/auth/login`
- `POST /api/auth/refresh`
- `POST /api/auth/logout`

### Users
- `GET /api/users/me`
- `PUT /api/users/me`
- `POST /api/users/me/profile`
- `GET /api/users/list?page=&limit=`

### Conversations
- `GET /api/conversations`
- `POST /api/conversations`

### Messages
- `GET /api/messages/:conversationId?limit=&beforeId=`
- `POST /api/messages`

### Message Status
- `POST /api/message-status/update`

### Uploads
- `POST /api/uploads/chat-file`

### Friends
- `POST /api/friends/request`
- `POST /api/friends/accept`
- `POST /api/friends/reject`
- `POST /api/friends/cancel`
- `GET /api/friends/pending?page=&limit=&query=`
- `GET /api/friends/pending-sent`
- `GET /api/friends/search?query=&limit=`
- `GET /api/friends/list?page=&limit=&query=`

## Socket Contract

### Client -> Server (chat)
- `join_conversation` `{ conversationId }`
- `typing` `{ conversationId, isTyping }`
- `send_message` `{ conversationId, messageType, messageText, fileUrl, clientMessageId }`
- `message_delivered` `{ messageId, conversationId }`
- `mark_as_read` `{ conversationId }`
- `sync` `{}`

### Server -> Client (chat)
- `receive_message`
- `message_sent_ack`
- `message_send_error`
- `message_status_update`
- `typing`
- `active_users`
- `sync_response` `{ activeUsers, unreadCounts }`
- `user_status` / `user_online` / `user_offline`

### Client -> Server (friends)
- `send_friend_request` `{ recipientId }`
- `accept_friend_request` `{ requesterId }`
- `reject_friend_request` `{ requesterId }`
- `cancel_friend_request` `{ recipientId }`

### Server -> Client (friends)
- `friend_request_received`
- `friend_request_accepted`
- `friend_list_updated`
- `friend_request_error`

## Frontend Architecture (`client/src/`)

### App Shell
- `App.vue`
  - Desktop (`lg+`): sidebar + routed right panel
  - Mobile: single panel flow for chats/friends/profile

### State Stores
- `stores/auth.js`
  - login/register/logout/fetchProfile
  - token persistence + socket connect
  - initializes chat + friend stores
- `stores/chat.js`
  - conversations/messages/typing/status
  - optimistic send + idempotency reconciliation
  - user list is now sourced from accepted friends (`/friends/list`)
- `stores/friends.js`
  - pending received/sent, friends list, search results
  - send/accept/reject/cancel request actions
- `stores/theme.js`
  - light/dark mode persistence

### Services
- `services/api.js` Axios instance (`/api`)
- `services/socketService.js` socket lifecycle + event listeners

### Main UI Components
- `components/ChatList.vue`
- `components/ChatWindow.vue`
- `components/MessageBubble.vue`
- `components/Sidebar.vue`
- `components/SettingsSidebar.vue` (Friends section)
  - Tabs: Pending Requests / Search Friends
- `components/ProfileSidebar.vue`
  - Profile edit
  - Dark mode toggle
  - Logout button

## Current Functional Rules
- Chat list shows accepted friends and keeps most-recent chat activity on top.
- Search Friends keeps users visible after sending request.
- Sent requests can be cancelled from Search Friends (`Remove Sent Request`).
- Pending received requests support Accept/Reject.
- Logout is in Profile section.
- Dark mode toggle is in Profile section (above logout).

## Environment Keys (Backend)
Required/used:
- `PORT`
- `NODE_ENV`
- `CLIENT_URL`
- `JWT_SECRET`
- `DB_HOST`
- `DB_USER`
- `DB_PASSWORD`
- `DB_NAME`

Optional:
- `SOCKET_PING_INTERVAL`
- `SOCKET_PING_TIMEOUT`
- `SOCKET_MAX_PAYLOAD`
- `REDIS_URL`
- `LOG_LEVEL`

## Runbook

### Backend
```bash
cd api
npm install
npm run dev
# or
npm run start
```

### Frontend
```bash
cd client
npm install
npm run dev
npm run build
```

## Safe Change Rules
- Do not break existing API/socket contracts used by chat/auth flows.
- Keep JWT access + refresh-cookie auth flow unchanged.
- Preserve message idempotency via `client_message_id`.
- If DB changes are needed, add a new migration file only; never rewrite applied migrations.
- Keep chat behavior unaffected when extending friends features.

## Validation Checklist
1. Backend starts and applies migrations (including `008_create_friendships.sql`).
2. Login/register/profile/logout work.
3. Chat conversations/messages/status/presence/typing still work.
4. Friends flow works:
   - search users
   - send request
   - remove sent request
   - receive pending request
   - accept/reject request
5. After accept, user appears in chat list.
6. Frontend build passes.
