Important
This project was created by @mvtbh. My only contribution was the backend, which is now open source. Aside from that, I was not involved in the development of this project.
The project has been discontinued and is no longer being worked on. It is also important to note that the project was never fully completed and remains unfinished. A significant amount of work would still be required to bring it to a complete and production-ready state.
As the project has been abandoned, I decided to release my contribution as open source.
The back-end source code can be found here: HERE
Modern React/Next.js messenger-style UI. This README explains how to connect the frontend to a backend API, which endpoints the app expects, where to integrate calls, and how to run the project in both demo and real modes.
- Node.js 18+ and pnpm/npm installed
- Clone repo, then install and run:
pnpm install
pnpm devThe app runs in demo mode by default (mocked data) until you provide real API/WS URLs.
Create a .env.local in the project root:
NEXT_PUBLIC_API_URL=https://api.your-domain.com
NEXT_PUBLIC_WS_URL=wss://ws.your-domain.comNotes:
- Demo mode is used when these variables are missing or point to localhost/demo (see
lib/api.tsisDemoMode()). - To use a real backend, set both variables to non-localhost URLs and restart the dev server.
- Authentication: register, login (optionally 2FA), JWT token issuance/validation
- Users: current user profile, user list, profiles, presence/online status
- Messages: global channel, direct messages, reactions, read receipts, pagination
- Groups: create group, list groups, group membership, group messages
- Presence & typing: real-time status via WebSocket (connect with JWT)
- File uploads: receive file uploads or issue pre-signed URLs; return downloadable URL + expiry
- Settings & privacy: read/write user preferences (optional; some settings are client-only today)
The current code calls a subset already. You can keep these paths and payloads to minimize changes.
- POST
/user/register
Request:
{ "username": "alice", "email": "alice@example.com", "password": "StrongPass1!" }Response 200:
{
"text": "Account Created!",
"token": "<jwt>",
"user": {
"id": "123",
"username": "alice",
"email": "alice@example.com",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}- POST
/user/login
Request:
{ "email": "alice@example.com", "password": "StrongPass1!", "twoFactorCode": "123456", "twoFactorToken": "optional" }Response 200 (normal):
{ "text": "Login successful", "token": "<jwt>", "user": { "id": "123", "username": "alice", "email": "alice@example.com", "createdAt": "..." } }Response 200 (2FA required):
{ "requiresTwoFactor": true, "twoFactorToken": "temp-2fa-token" }Optional 2FA endpoints (wire later to UI):
- POST
/user/2fa/enable{ code } - POST
/user/2fa/disable{ code }
- GET
/user/list
Response 200:
[
{ "id": "2", "username": "Alice", "email": "alice@pixel.com", "createdAt": "..." },
{ "id": "3", "username": "Bob", "email": "bob@pixel.com", "createdAt": "..." }
]Recommended (not yet called by code):
- GET
/user/me→ current user - GET
/user/:id→ profile - PATCH
/user/profile→ update profile - GET
/presenceor WS events for presence
- GET
/messages?page=0&limit=50
Response 200:
[
{
"id": "1",
"sendedById": 2,
"sendedByUsername": "Alice",
"message": "Hello world",
"receiverId": null,
"inGlobalChat": true,
"timestamp": "1710000000000"
}
]- POST
/messages/:dmId
Request:
{ "userId": 1 }Response 200: same array shape as above (DM history).
Suggested send endpoints (wire from ChatContext.sendMessage when you build it):
- POST
/messages(global) body: { message } - POST
/dms/:userId/messagesbody: { message }
- POST
/messages/:id/reactionsbody: { emoji } - DELETE
/messages/:id/reactionsbody: { emoji }
Response shape for a reaction the UI can merge:
{ "id": "rx1", "messageId": "1", "userId": 123, "username": "alice", "emoji": "👍", "timestamp": "1710000001000" }- POST
/friends/:userId - DELETE
/friends/:userId - POST
/blocks/:userId - DELETE
/blocks/:userId - POST
/dms/:userId/mute - DELETE
/dms/:userId/mute - POST
/dms/:userId/pin - DELETE
/dms/:userId/pin - GET
/dms→ return DM inbox summaries the UI can show asDMInfo[]
Example DMInfo item:
{ "userId": 2, "username": "Alice", "isOnline": true, "unreadCount": 3, "isPinned": false, "isMuted": false, "isBlocked": false, "pinnedMessages": [] }- POST
/groupsbody: { name, description?, members: string[] } - GET
/groups - GET
/groups/:id/messages - POST
/groups/:id/messagesbody: { message }
- POST
/files(multipart/form-data) → { url, expiresAt, fileName, size, contentType } Or implement pre-signed uploads: - POST
/files/presign→ { uploadUrl, url, expiresAt } then PUT the file touploadUrl.
- GET
/news?page=0&limit=50→ array of items{ id, title, content, author, timestamp, isPinned, tags }
Connect with wss://.../socket?token=<jwt> and send/receive JSON messages like:
{ "type": "message", "data": { "id": "...", "sendedById": 1, "sendedByUsername": "alice", "message": "...", "receiverId": null, "inGlobalChat": true, "timestamp": "..." } }Other event types to support:
typing{ userId, username, chatId, isTyping }presence{ userId, status }reaction{ messageId, ...reaction }read-receipt{ messageId, readerId, timestamp }
-
lib/api.ts- Primary HTTP client. Replace demo handlers by implementing real requests.
- Already implements:
register,login,getMessages,getDMMessages,getUsers. - Needs backend wiring:
updateProfile,addReaction,removeReaction, 2FA methods, friends/block/mute/pin, DM inbox:getDMInfos,getNewsMessages.
-
context/auth-context.tsx- Uses
apiClient.login/register, persists token/user tolocalStorage. - If your
/user/loginreturns a richeruser, ensure it matchestypes/User.
- Uses
-
context/chat-context.tsx- Loads users/DM info/messages via
apiClient. sendMessageis optimistic only; wire it to POST endpoints and merge the server echo/ack.- Friend/block/mute/pin functions call
apiClient.*placeholders—implement these.
- Loads users/DM info/messages via
-
lib/socket.ts- Contains a commented WebSocket client showing the expected event shapes.
- Uncomment and adapt once your WS server is ready.
-
components/ui/file-upload.tsxandlib/file-manager.ts- Currently simulate uploads with
URL.createObjectURL. - Replace
fileManager.uploadFile(file)with a call to your/filesor presign flow.
- Currently simulate uploads with
-
components/chat/create-group-modal.tsx- Calls
useChat().createGroup(...)—addcreateGroupto ChatContext andApiClientwhen backend is ready.
- Calls
You already have lib/api.ts. If you prefer a minimal services layer, this example shows token storage, error handling, and a couple endpoints.
// services/apiClient.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL!;
let authToken: string | null = null;
export function setAuthToken(token: string | null) { authToken = token; }
async function http(path: string, init: RequestInit = {}) {
const headers: HeadersInit = { 'Content-Type': 'application/json', ...(init.headers || {}) };
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
const res = await fetch(`${API_URL}${path}`, { ...init, headers });
const text = await res.text();
const data = text ? JSON.parse(text) : null;
if (!res.ok) throw new Error(data?.message || `HTTP ${res.status}`);
return data;
}
export const api = {
register: (username: string, email: string, password: string) =>
http('/user/register', { method: 'POST', body: JSON.stringify({ username, email, password }) }),
login: async (email: string, password: string, twoFactorCode?: string, twoFactorToken?: string) => {
const data = await http('/user/login', { method: 'POST', body: JSON.stringify({ email, password, twoFactorCode, twoFactorToken }) });
if (data.token) setAuthToken(data.token);
return data;
},
getUsers: () => http('/user/list'),
getMessages: (page = 0, limit = 50) => http(`/messages?page=${page}&limit=${limit}`),
getDMMessages: (dmId: number, userId: number) => http(`/messages/${dmId}`, { method: 'POST', body: JSON.stringify({ userId }) }),
sendMessage: (payload: { message: string; receiverId?: number | null }) => http('/messages', { method: 'POST', body: JSON.stringify(payload) }),
addReaction: (messageId: string, emoji: string) => http(`/messages/${messageId}/reactions`, { method: 'POST', body: JSON.stringify({ emoji }) }),
removeReaction: (messageId: string, emoji: string) => http(`/messages/${messageId}/reactions`, { method: 'DELETE', body: JSON.stringify({ emoji }) }),
};To use it, replace calls in lib/api.ts or import api in contexts/components and wire it similarly to the existing apiClient.
// services/socketClient.ts
type Message = { id: string; sendedById: number; sendedByUsername: string; message: string; receiverId?: number|null; inGlobalChat: boolean; timestamp: string };
export class SocketClient {
private socket: WebSocket | null = null;
connect(token: string) {
const WS_URL = process.env.NEXT_PUBLIC_WS_URL!;
this.socket = new WebSocket(`${WS_URL}?token=${encodeURIComponent(token)}`);
}
sendMessage(data: Message) {
this.socket?.send(JSON.stringify({ type: 'message', data }));
}
}Wire into context/chat-context.tsx after login and clean up on logout.
-
Auth
- Use JWT (RS256/HS256). Return
tokenanduserfrom/user/loginand/user/register. - Protect all non-auth endpoints with
Authorization: Bearer <token>.
- Use JWT (RS256/HS256). Return
-
CORS
- Allow the frontend origin and
Authorization, Content-Typeheaders. Include credentials if needed.
- Allow the frontend origin and
-
Pagination
/messagesacceptspageandlimit. Return arrays the UI maps directly totypes/MessageInterface.
-
Message fields
- The UI expects:
id,sendedById,sendedByUsername,message,receiverId(null for global),inGlobalChat,timestamp(ms as string). Include these in responses or adjustlib/api.tsmappers.
- The UI expects:
-
Presence/typing
- Prefer WS. Broadcast
typingandpresenceupdates to relevant subscribers.
- Prefer WS. Broadcast
-
Files
- Enforce size/type limits. Return downloadable
urlandexpiresAt. If using object storage, prefer a presign flow.
- Enforce size/type limits. Return downloadable
-
Security
- Validate/sanitize inputs server-side. Rate-limit auth/message endpoints. Consider audit logging for admin.
See the mappers in lib/api.ts:
if (endpoint === "/user/login") {
const body = JSON.parse(options.body as string);
return this.demoLogin(body.email, body.password);
}// Map backend message to frontend MessageInterface
private mapBackendMessage(backendMsg: any): MessageInterface {
return {
id: backendMsg.id,
sendedById: backendMsg.sendedById,
sendedByUsername: backendMsg.sendedByUsername,
message: backendMsg.message,
receiverId: backendMsg.receiverId,
inGlobalChat: backendMsg.inGlobalChat,
timestamp: backendMsg.timestamp,
encrypted: false,
reactions: [],
};
}If your backend uses different names, adjust these mappers.
- Set
.env.localwith real API/WS URLs - Implement endpoints listed above; return fields expected by
types/* - Ensure JWT auth on all protected routes and WS connection
- Update
lib/api.tsto use real endpoints and remove demo guards - Wire
sendMessageincontext/chat-context.tsxto POST/WS - Replace
lib/file-manager.tswith actual upload calls - Uncomment and adapt
lib/socket.tsonce your WS is ready - Validate CORS and test via
pnpm dev
pnpm install
cp .env.local.example .env.local # or create one as shown above
pnpm devOpen http://localhost:3000/. With valid env URLs and a running backend, you’ll see real data. Without them, you’ll see demo data.