Project Overview
This repository implements a language-exchange video-calling app with real-time chat, video calls, social graph features and customizable theming. It uses a two-tier architecture—an Express/MongoDB backend and a React/Vite frontend—leveraging Stream SDKs for chat and calling. Developers can deploy, customize or extend this platform to build interactive language-learning or community apps.
Key Features
- Real-time text chat and 1:1 or group video calls via Stream SDK
- User profiles and social graph (follow/unfollow, friend lists)
- Dynamic theming (light/dark mode, custom brand colors)
- Authentication, user management and persistent chat history
- Responsive React UI with routing and global state management
Architecture Overview
Backend (Express + MongoDB)
- server.js initializes an Express server with CORS, JSON middleware and error handlers
- Routes:
- /auth – sign up, login, token refresh
- /users – profile CRUD, follow/unfollow
- /chat – message history, channel management
- Integrates Stream Chat & Video SDKs for messaging and WebRTC calls
- Serves frontend build in production
- Connects to MongoDB on startup
Frontend (React + Vite)
- main.jsx bootstraps the React app:
- BrowserRouter for client-side routing
- React Query for data fetching and caching
- GlobalStyles and ThemeProvider for theming
- App component defines routes for login, chat rooms and video call pages
- Uses Stream React SDK components for chat UI and video call orchestration
Primary Use Cases
- Instant language practice sessions with text support
- Group study or tutoring with video conferencing
- Community-centric platforms with follow/friend features
- White-label apps requiring custom branding and themes
Audience
This project targets developers who want to:
- Deploy a turnkey video-calling/chat application
- Extend or rebrand the UI, authentication flow or messaging logic
- Integrate additional features (e.g., file sharing, notifications)
- Adapt the architecture for other real-time collaboration scenarios
Quick Start Scripts
Clone the repo, install dependencies, then in two terminals:
# Start backend in development mode
npm run dev:backend
# Start frontend with hot reload
npm run dev:frontend
For production builds:
# Build both tiers
npm run build:backend
npm run build:frontend
# Start the production server
npm start
## 2. Quick Start & Setup
Get the app running locally in minutes. This section covers cloning the repo, configuring environment variables, installing dependencies, and running both backend and frontend in development and production modes.
### 2.1 Prerequisites
- Node.js v16+ and npm (or Yarn)
- A MongoDB instance (local or hosted)
- Stream Chat account (API Key & Secret)
### 2.2 Clone the Repository
Run:
```bash
git clone https://github.com/sok97/videocallingapp.git
cd videocallingapp
2.3 Configure Environment Variables
2.3.1 Backend
Create backend/.env
with:
# MongoDB connection string
MONGODB_URI=mongodb+srv://<user>:<pass>@cluster0.mongodb.net/videocalling
# Stream Chat credentials
STREAM_API_KEY=your_stream_api_key
STREAM_API_SECRET=your_stream_api_secret
# JWT secret for auth tokens
JWT_SECRET=your_jwt_secret
# Server port (optional, defaults to 4000)
PORT=4000
2.3.2 Frontend
Create frontend/.env
with:
# Stream Chat public key
VITE_STREAM_API_KEY=your_stream_api_key
# Base URL of backend API
VITE_BACKEND_URL=http://localhost:4000
The frontend fetches chat tokens from
${VITE_BACKEND_URL}/chat/token
and other endpoints under${VITE_BACKEND_URL}
.
2.4 Install Dependencies
Install backend and frontend packages:
# In project root
cd backend
npm install
cd ../frontend
npm install
2.5 Development Mode
2.5.1 Run Backend
cd backend
npm run dev
- Starts Express with
nodemon src/server.js
- Connects to MongoDB via
src/lib/db.js
- Enables CORS and serves auth/chat routes
2.5.2 Run Frontend
cd frontend
npm run dev
- Starts Vite dev server at
http://localhost:5173
- Uses your Stream API key and backend URL from
.env
2.6 Production Build & Serve
2.6.1 Build Frontend
cd frontend
npm run build
- Outputs static files to
frontend/dist
2.6.2 Start Backend in Production
cd backend
npm run start
- Serves API and static frontend files (from
../frontend/dist
) - Listens on
PORT
from.env
Visit http://localhost:4000
to access the production bundle.
2.7 Common Troubleshooting
MongoDB connection errors
• VerifyMONGODB_URI
is correct and network/firewall allows access.
• Inspect startup logs inbackend/src/lib/db.js
.CORS issues
• By default, server appliescors()
without origin whitelist.
• To restrict origins, updatesrc/server.js
:app.use(cors({ origin: 'http://localhost:5173' }));
JWT authentication failures
• ConfirmJWT_SECRET
matches between backend and any token-generating clients.
• Check token payload with jwt.io.Stream Chat errors
• EnsureSTREAM_API_KEY
andSTREAM_API_SECRET
are valid in both backend and frontend env files.
• Review responses in browser dev tools or backend logs.Port conflicts
• ChangePORT
inbackend/.env
or Vite dev port viafrontend/vite.config.js
.
With these steps you’ll have a local development environment and a production-ready build for the videocallingapp. Happy coding!
3. Backend API Reference
Authentication Endpoints
POST /api/auth/signup
Registers a new user, hashes password, issues JWT cookie.
• Method: POST
• URL: /api/auth/signup
• Auth: none
• Headers:
- Content-Type: application/json
Request Body
{
"email": "user@example.com",
"password": "securePassword123",
"fullName": "John Doe" // optional
}
Success Response (201)
Sets Set-Cookie: jwt=<token>; HttpOnly
{
"success": true,
"user": {
"_id": "60f7a3c2a5e4e12d4c8b4567",
"email": "user@example.com",
"fullName": "John Doe",
"isOnboarded": false,
// ...
}
}
Error Responses
- 400 Bad Request: missing required fields
- 409 Conflict: email already in use
- 500 Internal Server Error
Example Curl
curl -i -X POST https://api.videocallapp.com/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"securePassword123","fullName":"John Doe"}'
POST /api/auth/login
Validates credentials, issues JWT cookie.
• Method: POST
• URL: /api/auth/login
• Auth: none
• Headers: Content-Type: application/json
Request Body
{
"email": "user@example.com",
"password": "securePassword123"
}
Success Response (200)
Sets Set-Cookie: jwt=<token>; HttpOnly
{
"success": true,
"user": {
"_id": "60f7a3c2a5e4e12d4c8b4567",
"email": "user@example.com",
"fullName": "John Doe",
"isOnboarded": true
}
}
Error Responses
- 400 Bad Request: missing email/password
- 401 Unauthorized: invalid credentials
- 500 Internal Server Error
Example Curl
curl -i -X POST https://api.videocallapp.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"securePassword123"}'
POST /api/auth/logout
Clears the authentication cookie.
• Method: POST
• URL: /api/auth/logout
• Auth: none
• Headers: Cookie: jwt=<token>
Success Response (200)
Clears jwt
cookie
{
"success": true,
"message": "Logged out successfully"
}
Error Responses
- 400 Bad Request: no token provided
Example Curl
curl -i -X POST https://api.videocallapp.com/api/auth/logout \
-H "Cookie: jwt=<your_jwt_token>"
GET /api/auth/me
Fetches current user profile.
• Method: GET
• URL: /api/auth/me
• Auth: Protected (Cookie: jwt=<token>
)
Success Response (200)
{
"_id": "60f7a3c2a5e4e12d4c8b4567",
"email": "user@example.com",
"fullName": "John Doe",
"isOnboarded": true,
// other profile fields
}
Error Responses
- 401 Unauthorized: missing/invalid token
Example Curl
curl -i https://api.videocallapp.com/api/auth/me \
-H "Cookie: jwt=<your_jwt_token>"
POST /api/auth/onboarding
Collects additional profile info, updates MongoDB and Stream.
• Method: POST
• URL: /api/auth/onboarding
• Auth: Protected
• Headers:
- Content-Type: application/json
- Cookie:
jwt=<token>
Request Body
{
"fullName": "Jane Doe",
"bio": "Software engineer and polyglot",
"nativeLanguage": "English",
"learningLanguage": "Spanish",
"location": "Berlin, Germany"
}
Success Response (200)
{
"success": true,
"message": "User onboarded successfully",
"user": {
"_id": "60f7a3c2a5e4e12d4c8b4567",
"fullName": "Jane Doe",
"email": "jane@example.com",
"bio": "Software engineer and polyglot",
"nativeLanguage": "English",
"learningLanguage": "Spanish",
"location": "Berlin, Germany",
"isOnboarded": true,
"profilePic": "https://avatar.iran.liara.run/public/42.png"
}
}
Error Responses
- 400 Missing fields array
- 401 Unauthorized
- 404 User not found
- 500 Internal Server Error
Example Curl
curl -i -X POST https://api.videocallapp.com/api/auth/onboarding \
-H "Content-Type: application/json" \
-H "Cookie: jwt=<your_jwt_token>" \
-d '{"fullName":"Jane Doe","bio":"Software engineer","nativeLanguage":"English","learningLanguage":"Spanish","location":"Berlin"}'
Chat Endpoint
GET /api/chat/token
Returns a Stream Chat token for the authenticated user.
• Method: GET
• URL: /api/chat/token
• Auth: Protected (Cookie: jwt=<token>
)
Success Response (200)
{
"token": "<stream_token>"
}
Error Responses
- 401 Unauthorized
- 500 Internal Server Error
Example Curl
curl -i https://api.videocallapp.com/api/chat/token \
-H "Cookie: jwt=<your_jwt_token>"
User & Friendship Endpoints
GET /api/users/recommended
Fetches recommended users for friendship.
• Method: GET
• URL: /api/users/recommended
• Auth: Protected
Success Response (200)
[
{
"_id": "60f1a2b3c4d5e6f7a8b9c0d1",
"fullName": "Alice Smith",
"nativeLanguage": "French",
"learningLanguage": "English",
"profilePic": "https://..."
},
…
]
Error Responses
- 401 Unauthorized
- 500 Internal Server Error
Example Curl
curl -i https://api.videocallapp.com/api/users/recommended \
-H "Cookie: jwt=<your_jwt_token>"
GET /api/users/friends
Retrieves the current user's friends list.
• Method: GET
• URL: /api/users/friends
• Auth: Protected
Success Response (200)
[
{
"_id": "60f1a2b3c4d5e6f7a8b9c0d2",
"fullName": "Bob Lee",
"nativeLanguage": "Spanish",
"learningLanguage": "German",
"profilePic": "https://..."
},
…
]
Error Responses
- 401 Unauthorized
- 500 Internal Server Error
Example Curl
curl -i https://api.videocallapp.com/api/users/friends \
-H "Cookie: jwt=<your_jwt_token>"
POST /api/users/friend-request/:recipientId
Sends a friend request.
• Method: POST
• URL: /api/users/friend-request/:recipientId
• Auth: Protected
Success Response (201)
{
"_id": "610a1b2c3d4e5f6a7b8c9d0e",
"sender": "60f1a2b3c4d5e6f7a8b9c0d1",
"recipient": "60f1a2b3c4d5e6f7a8b9c0d2",
"status": "pending",
"createdAt": "...",
"updatedAt": "..."
}
Error Responses
- 400 Cannot request self/existing friend/duplicate
- 404 Recipient not found
- 401 Unauthorized
Example Curl
curl -i -X POST https://api.videocallapp.com/api/users/friend-request/60f1a2b3c4d5e6f7a8b9c0d2 \
-H "Cookie: jwt=<your_jwt_token>"
POST /api/users/friend-request/:requestId/accept
Accepts a pending friend request.
• Method: POST
• URL: /api/users/friend-request/:requestId/accept
• Auth: Protected
Success Response (200)
{ "message": "Friend request accepted" }
Error Responses
- 403 Not recipient
- 404 Request not found
- 401 Unauthorized
Example Curl
curl -i -X POST https://api.videocallapp.com/api/users/friend-request/610a1b2c3d4e5f6a7b8c9d0e/accept \
-H "Cookie: jwt=<your_jwt_token>"
GET /api/users/friend-requests
Fetches incoming pending and accepted sent requests.
• Method: GET
• URL: /api/users/friend-requests
• Auth: Protected
Success Response (200)
{
"incomingReqs": [
{
"_id": "...",
"sender": {
"_id": "...",
"fullName": "...",
"profilePic": "...",
"nativeLanguage": "...",
"learningLanguage": "..."
},
"status": "pending",
"createdAt": "..."
}
],
"acceptedReqs": [
{
"_id": "...",
"recipient": {
"_id": "...",
"fullName": "...",
"profilePic": "..."
},
"status": "accepted",
"createdAt": "..."
}
]
}
Error Responses
- 401 Unauthorized
- 500 Internal Server Error
Example Curl
curl -i https://api.videocallapp.com/api/users/friend-requests \
-H "Cookie: jwt=<your_jwt_token>"
GET /api/users/outgoing-friend-requests
Retrieves all pending requests sent by the user.
• Method: GET
• URL: /api/users/outgoing-friend-requests
• Auth: Protected
Success Response (200)
[
{
"_id": "...",
"recipient": {
"_id": "...",
"fullName": "...",
"profilePic": "...",
"nativeLanguage": "...",
"learningLanguage": "..."
},
"status": "pending",
"createdAt": "..."
}
]
Error Responses
- 401 Unauthorized
- 500 Internal Server Error
Example Curl
curl -i https://api.videocallapp.com/api/users/outgoing-friend-requests \
-H "Cookie: jwt=<your_jwt_token>"
## Frontend Guide
This section covers the React/Vite frontend’s core patterns: client-side routing with access control, HTTP communication via Axios and a custom API wrapper, state management with React Query and Zustand, theming, and integrating Stream Chat and Video Calling components.
---
### Routing and Access Control (App.jsx)
Configure routes with React Router, guarding pages based on authentication (`authUser`) and onboarding status (`authUser.isOnboarded`).
#### Core Pattern
```jsx
// frontend/src/App.jsx
import { Routes, Route, Navigate } from 'react-router-dom';
import useAuthUser from './hooks/useAuthUser';
import Layout from './components/Layout';
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import OnboardingPage from './pages/OnboardingPage';
// …other imports
function App() {
const { isLoading, authUser } = useAuthUser();
const isAuthenticated = Boolean(authUser);
const isOnboarded = authUser?.isOnboarded;
if (isLoading) return <div>Loading…</div>;
return (
<Routes>
{/* Protected Home */}
<Route
path="/"
element={
isAuthenticated && isOnboarded
? <Layout showSidebar><HomePage /></Layout>
: <Navigate to={isAuthenticated ? "/onboarding" : "/login"} />
}
/>
{/* Onboarding */}
<Route
path="/onboarding"
element={
isAuthenticated
? (!isOnboarded ? <OnboardingPage /> : <Navigate to="/" />)
: <Navigate to="/login" />
}
/>
{/* Public Auth */}
<Route
path="/login"
element={
!isAuthenticated
? <LoginPage />
: <Navigate to={isOnboarded ? "/" : "/onboarding"} />
}
/>
{/* Add other routes (Chat, Notifications, Signup, Call) using the same pattern */}
</Routes>
);
}
export default App;
Tips
- Wrap protected pages in
<Layout showSidebar>…</Layout>
. - Redirect un-authenticated users to
/login
and non-onboarded users to/onboarding
. - Prevent logged-in users from seeing auth pages by redirecting them to
/
or/onboarding
. - For maintainability, extract a
<ProtectedRoute>
component acceptingrequiresOnboarding
andshowSidebar
props.
HTTP Layer: Axios Instance & API Wrapper
Centralize API endpoints and keep HTTP logic out of components.
Axios Configuration
// frontend/src/libs/axios.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true, // send cookies for auth
headers: { 'Content-Type': 'application/json' },
});
export default apiClient;
API Wrapper Functions
// frontend/src/libs/api.js
import apiClient from './axios';
export const signup = (data) => apiClient.post('/auth/signup', data);
export const login = (data) => apiClient.post('/auth/login', data);
export const logout = () => apiClient.post('/auth/logout');
export const getAuthUser = () => apiClient.get('/auth/me');
export const completeOnboarding = (payload) =>
apiClient.post('/auth/onboarding', payload);
export const getFriends = () =>
apiClient.get('/friends'); // fetch friend list
export const addFriend = (id) =>
apiClient.post(`/friends/${id}/add`);
export const getChatToken = () =>
apiClient.get('/stream/chat/token'); // returns { token, user }
export const getVideoToken = (id) =>
apiClient.post(`/stream/video/token`, { callId: id });
Usage Example
import { useMutation, useQuery } from '@tanstack/react-query';
import { login, getFriends } from '../libs/api';
function LoginForm() {
const mutation = useMutation(login, {
onSuccess: () => queryClient.invalidateQueries(['authUser']),
});
const handleSubmit = (e) => {
e.preventDefault();
const form = new FormData(e.target);
mutation.mutate({
email: form.get('email'),
password: form.get('password'),
});
};
return (
<form onSubmit={handleSubmit}>
{/* email/password inputs */}
<button disabled={mutation.isLoading}>Log in</button>
</form>
);
}
function FriendsList() {
const { data, isLoading } = useQuery(['friends'], getFriends);
if (isLoading) return <div>Loading friends…</div>;
return (
<ul>
{data.data.map(f => <li key={f.id}>{f.name}</li>)}
</ul>
);
}
Authentication State with React Query (useAuthUser)
Wraps getAuthUser
in a hook that returns isLoading
and authUser
. Disables retries on 401.
// frontend/src/hooks/useAuthUser.js
import { useQuery } from '@tanstack/react-query';
import { getAuthUser } from '../libs/api';
export default function useAuthUser() {
const { data, isLoading } = useQuery({
queryKey: ['authUser'],
queryFn: () => getAuthUser().then(res => res.data.user),
retry: false,
});
return { isLoading, authUser: data };
}
Usage
function Dashboard() {
const { isLoading, authUser } = useAuthUser();
if (isLoading) return <div>Loading…</div>;
if (!authUser) return <Navigate to="/login" replace />;
return <h1>Welcome, {authUser.name}</h1>;
}
Invalidate with useQueryClient().invalidateQueries(['authUser'])
after login/logout.
Global Theming with Zustand (useThemeStore)
Persist theme selection in localStorage
and apply via a data-theme
attribute on <html>
.
// frontend/src/store/useThemeStore.js
import { create } from 'zustand';
export const useThemeStore = create(set => ({
theme: localStorage.getItem('mytheme') || 'light',
setTheme: theme => {
set({ theme });
localStorage.setItem('mytheme', theme);
document.documentElement.setAttribute('data-theme', theme);
},
}));
Theme Constants
// frontend/src/constants/index.js
export const THEMES = [
{ name: 'light', label: 'Light', colors: ['#fff','#4a5568','#2d3748','#1a202c'] },
{ name: 'dark', label: 'Dark', colors: ['#1a202c','#2d3748','#4a5568','#fff'] },
// …add more
];
Applying Theme on App Init
// frontend/src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useThemeStore } from './store/useThemeStore';
function Root() {
const { theme } = useThemeStore();
React.useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return <App />;
}
const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<Root />
</QueryClientProvider>,
document.getElementById('root')
);
Stream Chat Integration
Initialize Stream Chat client with a token from your API, then render chat components.
// frontend/src/pages/ChatPage.jsx
import React, { useEffect, useState } from 'react';
import { StreamChat } from 'stream-chat';
import {
Chat,
Channel,
ChannelHeader,
MessageList,
MessageInput,
} from 'stream-chat-react';
import { useQuery } from '@tanstack/react-query';
import { getChatToken } from '../libs/api';
import useAuthUser from '../hooks/useAuthUser';
function ChatPage() {
const { authUser, isLoading: authLoading } = useAuthUser();
const { data: tokenData, isLoading: tokenLoading } = useQuery(
['chatToken'],
getChatToken,
{ enabled: !!authUser }
);
const [chatClient, setChatClient] = useState(null);
useEffect(() => {
if (!tokenData || !authUser) return;
const client = StreamChat.getInstance(import.meta.env.VITE_STREAM_CHAT_API_KEY);
client.connectUser(
{ id: authUser._id, name: authUser.fullName, image: authUser.profilePic },
tokenData.data.token
);
setChatClient(client);
}, [tokenData, authUser]);
if (authLoading || tokenLoading || !chatClient) return <div>Loading chat…</div>;
const channel = chatClient.channel('messaging', 'global');
return (
<Chat client={chatClient} theme="messaging light">
<Channel channel={channel}>
<ChannelHeader />
<MessageList />
<MessageInput />
</Channel>
</Chat>
);
}
export default ChatPage;
Key Points
- Fetch a chat token via React Query, then
StreamChat.getInstance(...).connectUser()
. - Use
<Chat>
and<Channel>
wrappers to render messages. - Always clean up the client on unmount:
return () => chatClient.disconnectUser();
if needed.
Video Call Integration
Wire a call button to generate a call link, send it in chat, and implement the call page with Stream Video.
Sending a Call Link
// in ChatPage.jsx, inside your Channel window
import CallButton from '../components/CallButton';
import { toast } from 'react-toastify';
function ChatWithCall({ channel }) {
const handleVideoCall = () => {
const callLink = `${window.location.origin}/call/${channel.id}`;
channel.sendMessage({ text: `🔴 Join call: ${callLink}` });
toast.success('Call link sent');
};
return (
<div className="relative">
<CallButton handleVideoCall={handleVideoCall} />
{/* <Window><MessageList /><MessageInput /></Window> */}
</div>
);
}
Call Page Implementation
// frontend/src/pages/CallPage.jsx
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import {
StreamVideoClient,
StreamVideo,
StreamCall,
CallControls,
SpeakerLayout,
StreamTheme,
useCallStateHooks,
CallingState,
} from '@stream-io/video-react-sdk';
import useAuthUser from '../hooks/useAuthUser';
import { getVideoToken } from '../libs/api';
export default function CallPage() {
const { callId } = useParams();
const navigate = useNavigate();
const { authUser, isLoading: authLoading } = useAuthUser();
const { data: tokenData, isLoading: tokenLoading } = useQuery(
['videoToken', callId],
() => getVideoToken(callId).then(res => res.data.token),
{ enabled: !!authUser }
);
const [client, setClient] = useState(null);
const [call, setCall] = useState(null);
useEffect(() => {
if (!authUser || !tokenData || !callId) return;
(async () => {
const videoClient = new StreamVideoClient({
apiKey: import.meta.env.VITE_STREAM_VIDEO_API_KEY,
user: {
id: authUser._id,
name: authUser.fullName,
image: authUser.profilePic,
},
token: tokenData,
});
const callInstance = videoClient.call('default', callId);
await callInstance.join({ create: true });
setClient(videoClient);
setCall(callInstance);
})().catch(() => navigate('/', { replace: true }));
}, [authUser, tokenData, callId, navigate]);
if (authLoading || tokenLoading || !client || !call) {
return <div>Loading call…</div>;
}
return (
<StreamVideo client={client}>
<StreamCall call={call}>
<CallContent />
</StreamCall>
</StreamVideo>
);
}
function CallContent() {
const { useCallCallingState } = useCallStateHooks();
const state = useCallCallingState();
const navigate = useNavigate();
if (state === CallingState.LEFT) {
navigate('/', { replace: true });
return null;
}
return (
<StreamTheme>
<SpeakerLayout />
<CallControls />
</StreamTheme>
);
}
Highlights
- Extract
:callId
from URL viauseParams()
. - Fetch a video token before creating the
StreamVideoClient
. - Call
join({ create: true })
to auto-create the room if it doesn’t exist. - Monitor
CallingState.LEFT
to redirect participants when the call ends.
With these patterns, you can extend your app’s routing, global state, theming, and real-time chat/video calling features in a consistent, maintainable way.
5. Development & Contribution Guide
This guide covers repository layout, coding standards, Git workflow, environment setup, local development, and deployment notes.
5.1 Repository Structure
/
├── backend/ # Node.js + TypeScript API
│ ├── src/
│ ├── package.json # scripts: dev, start
│ ├── tsconfig.json
│ └── .env.example
├── frontend/ # React + Vite UI
│ ├── src/
│ ├── public/
│ ├── package.json # scripts: dev, build, lint, preview
│ ├── vite.config.js
│ ├── eslint.config.js
│ └── .env.example
├── package.json # root scripts: install, dev, build, start
└── README.md
5.2 Coding Standards
ESLint
- Frontend uses
frontend/eslint.config.js
. - Run lint:
cd frontend npm run lint
- Autofix issues:
npm run lint -- --fix
Prettier
- Install at root:
npm install --save-dev prettier
- Add
.prettierrc
:{ "singleQuote": true, "trailingComma": "es5", "printWidth": 80 }
- Format files:
npx prettier --write .
5.3 Git Workflow
- Create a feature branch off
main
:git checkout main git pull git checkout -b feature/your-feature
- Commit often with clear messages:
git commit -m "feat(auth): add JWT login endpoint"
- Push and open a Pull Request targeting
main
. - Ensure CI passes lint/build before merging.
- Delete your branch after merge.
5.4 Environment Setup
Backend .env.example
PORT=5000
MONGODB_URI=mongodb://localhost:27017/videocall
JWT_SECRET=your_jwt_secret
STREAM_API_KEY=your_stream_api_key
STREAM_API_SECRET=your_stream_api_secret
Frontend .env.example
VITE_API_URL=http://localhost:5000/api
VITE_STREAM_KEY=your_stream_api_key
VITE_STREAM_SECRET=your_stream_api_secret
Copy each to .env
and fill in real values.
5.5 Local Development
- Install all dependencies and run both services:
npm run install # root: installs backend & frontend deps npm run dev # runs dev servers concurrently
- Backend dev server runs on
http://localhost:5000
- Frontend dev server runs on
http://localhost:3000
5.6 Deployment Notes
Build Scripts
- Frontend:
cd frontend npm run build # outputs to frontend/dist
- Backend (TypeScript compile + start):
cd backend npm run build # tsc → dist/ npm run start # node dist/index.js
Static Serving
- Serve
frontend/dist
with Nginx, Vercel, or any static host. - Ensure
VITE_API_URL
points to your production API.
Production Environment Variables
- Define secrets in your host’s environment (e.g., Docker, Kubernetes Secrets).
- Never commit real secrets to Git.
- Example Docker Compose snippet:
services: backend: image: videocallingapp-backend:latest environment: - MONGODB_URI=${MONGODB_URI} - JWT_SECRET=${JWT_SECRET} - STREAM_API_KEY=${STREAM_API_KEY} - STREAM_API_SECRET=${STREAM_API_SECRET} frontend: image: videocallingapp-frontend:latest environment: - VITE_API_URL=${VITE_API_URL}
- Rebuild images after any code or environment-variable changes.