Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
npm-debug.log
.env
.git
75 changes: 75 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# First Contact - Backend Service

The backend service is a lightweight Node.js, Express, and Socket.io server that acts as a real-time message relay and interface to Apache CouchDB. It operates as part of the offline-first mesh communications system.

---

## ⚡ How Phase 3 (Real-time WebSockets) Works

Phase 3 implements instant, bidirectional messaging between devices on the local network using **Socket.io**. This allows peers to communicate instantly without poll-latency or internet dependencies.

### Architecture Flow
1. **Connection**: When a client loads the frontend, it establishes a WebSocket connection to the backend.
2. **Device Registration (`register_device`)**:
* The client emits this event sending its unique `deviceId` (stored in localStorage) and `username`.
* The backend maps the connection (`socket.id`) to the device details in an in-memory `onlineUsers` Map.
* The backend immediately broadcasts the updated active user list (`active_users_update`) to **all** connected clients.
3. **Message Relaying (`send_message`)**:
* A client sends a message payload: `{ _id, senderId, senderName, text, timestamp }`.
* The backend validates the payload and broadcasts it to all **other** sockets via the `receive_message` event.
* *Note: The sender adds the message to their own UI state immediately (optimistic UI), so the server uses `socket.broadcast.emit` to avoid sending it back to the initiator.*
4. **Disconnection (`disconnect`)**:
* When a browser tab is closed or a network drop occurs, the backend detects the disconnect.
* The corresponding user is removed from the `onlineUsers` Map.
* The server broadcasts the updated online user list to all remaining active clients.

---

## 🚀 Running the Server

### Option 1: Running Locally (Development Mode)

Ensure you have **Node.js** (v18+) and **npm** installed.

1. **Install dependencies**:
```bash
npm install
```

2. **Start the development server (with hot-reloading via Nodemon)**:
```bash
npm run dev
```
The backend will be available at `http://localhost:5000`.

---

### Option 2: Running with Docker (Individually)

You can containerize the backend on its own.

1. **Build the Docker Image**:
```bash
docker build -t first-contact-backend .
```

2. **Run the Container**:
```bash
docker run -d -p 5000:5000 --name first_contact_backend_container --env-file .env first-contact-backend
```

---

### Option 3: Running the Full Stack (Recommended)

To run the CouchDB instance, the Backend, and the Frontend together, use Docker Compose from the root workspace directory:

```bash
# Run from the project root
docker compose up --build
```

This starts:
* **CouchDB**: `http://localhost:5984`
* **Backend**: `http://localhost:5000`
* **Frontend**: `http://localhost:5173`
11 changes: 3 additions & 8 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,9 @@ const messageRoutes = require('./routes/messageRoutes');
app.use('/api/users', userRoutes);
app.use('/api/messages', messageRoutes);

// TODO: Import and use socket logic from sockets/chatSocket.js
io.on('connection', (socket) => {
console.log(`A user connected: ${socket.id}`);

socket.on('disconnect', () => {
console.log(`User disconnected: ${socket.id}`);
});
});
// Initialize all Socket.io event listeners from the chatSocket module
const chatSocket = require('./sockets/chatSocket');
chatSocket(io);

// Use 0.0.0.0 to listen on all local network interfaces (required for mesh setup)
const PORT = process.env.PORT || 5000;
Expand Down
100 changes: 95 additions & 5 deletions backend/sockets/chatSocket.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,97 @@
// chatSocket.js - Socket.io event listeners for real-time messaging

// module.exports = (io, socket) => {
// socket.on('sendMessage', (message) => {
// // save to db, then emit to everyone
// });
// };
/**
* In-memory map tracking currently connected sockets.
* Key: socket.id (assigned by socket.io on each connection)
* Value: { deviceId: string, username: string }
*/
const onlineUsers = new Map();

/**
* Builds a serializable array of active users from the in-memory map,
* suitable for broadcasting to all clients.
*/
const getActiveUserList = () => {
return Array.from(onlineUsers.values()).map(({ deviceId, username }) => ({
_id: deviceId,
username,
status: 'online',
joinedAt: new Date().toISOString(),
}));
};

/**
* Initializes all socket.io event listeners.
* @param {import('socket.io').Server} io - The socket.io Server instance
*/
module.exports = (io) => {
io.on('connection', (socket) => {
console.log(`[Socket] Client connected: ${socket.id}`);

// ------------------------------------------------------------------
// register_device
// Sent by the frontend after login. Links this socket to the user
// and broadcasts the updated active user list to ALL clients.
// Payload: { deviceId: string, username: string }
// ------------------------------------------------------------------
socket.on('register_device', ({ deviceId, username }) => {
if (!deviceId || !username) {
console.warn(`[Socket] register_device missing fields from ${socket.id}`);
return;
}

// Store the mapping
onlineUsers.set(socket.id, { deviceId, username });
console.log(`[Socket] Device registered: ${username} (${deviceId}) via socket ${socket.id}`);

// Push updated user list to every connected client
io.emit('active_users_update', getActiveUserList());
});

// ------------------------------------------------------------------
// send_message
// Receives a message payload from the sender and broadcasts it to
// all OTHER connected clients via receive_message.
// Payload: { _id, senderId, senderName, text, timestamp }
// ------------------------------------------------------------------
socket.on('send_message', (message) => {
const { senderId, senderName, text, timestamp } = message;

// Basic validation
if (!senderId || !senderName || !text) {
console.warn(`[Socket] send_message received invalid payload from ${socket.id}:`, message);
return;
}

const outgoing = {
_id: message._id || `${timestamp}-${senderId}`,
senderId,
senderName,
text,
timestamp: timestamp || new Date().toISOString(),
};

console.log(`[Socket] Relaying message from ${senderName}: "${text.slice(0, 40)}"`);

// Broadcast to everyone EXCEPT the sender (sender already added
// the message to their own state via optimistic update)
socket.broadcast.emit('receive_message', outgoing);
});

// ------------------------------------------------------------------
// disconnect
// Fired when a client closes the tab / loses connection.
// Removes from the map and notifies all remaining clients.
// ------------------------------------------------------------------
socket.on('disconnect', (reason) => {
const user = onlineUsers.get(socket.id);
if (user) {
console.log(`[Socket] Device disconnected: ${user.username} — reason: ${reason}`);
onlineUsers.delete(socket.id);
io.emit('active_users_update', getActiveUserList());
} else {
console.log(`[Socket] Unknown socket disconnected: ${socket.id}`);
}
});
});
};
5 changes: 5 additions & 0 deletions frontend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
npm-debug.log
dist
.env
.git
Loading