Especially Chapter 12, which will solidify your understanding of real-time web applications (WebSocket).
A single TCP handshake opens a permanent WebSocket connection that stays alive, allowing instant bi-directional data flow.Server-Push Updates: Instead of you requesting data via refresh, the server pushes events (task:created, task:moved) directly to all connected clients in the boardRoom.Optimistic Drag-and-Drop:Your UI updates instantly when you drag a card (Optimistic UI).The app emits a task:move event over Socket.io.The server updates MongoDB and broadcasts task:moved to everyone else in the room (socket.to).Fractional Indexing: Reordering uses simple float averages (
The Bi-directional behavior means that the connection between the client & server is Full-Duplex, client doesn't need to perform polling requests.
This concepts you would be familiar with if you treated with low-level MCU befor :)
A Trello/Asana-style tool: Workspaces → Boards → Lists → Tasks, with JWT auth (access + rotating refresh tokens) and real-time drag-and-drop powered by Socket.io.
- Node.js 18+
- A running MongoDB instance — either local (
mongodon27017) or a free MongoDB Atlas cluster. This sandbox couldn't spin up a live Mongo instance to test against (no network route to MongoDB's binary host), so you'll want to run this against your own Mongo before considering it verified end-to-end — see "What's been tested" below for exactly what has and hasn't been exercised.
Terminal 1 — backend
cd taskforge-backend
npm install
cp .env.example .env
# edit .env: set MONGO_URI to your Mongo instance, generate real secrets:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
npm run devBackend runs on http://localhost:5000.
Terminal 2 — frontend
cd taskforge-client
npm install
cp .env.example .env # defaults already point at localhost:5000
npm run devFrontend runs on http://localhost:5173.
- Open
http://localhost:5173→ redirected to/login. - Register a new account → redirected to
/dashboard(Workspaces). - Create a workspace → click into it → create a board.
- Board opens with three columns: To Do / In Progress / Done.
- Add a card in "To Do", drag it into "In Progress" — position persists
on refresh (confirms the REST
getBoard+ drag persistence work). - Real-time check: open the same board URL in a second browser (or an incognito window, logged in as a second registered user who's been invited to the workspace — see below). Drag a card in one window; it should move in the other window within a beat, no refresh needed. The "Live" badge in the header should read green once the socket connects.
- To test multi-user: from window A, use the workspace's "invite member"
flow (currently API-only — see
workspaceApi.inviteMember, no UI button yet) or just register two accounts and manually add the second user's id viaPOST /api/v1/workspaces/:id/memberswith their email. - Refresh the page mid-session → should NOT bounce to
/login(confirms the silent-refresh-on-load flow inAuthContext). - Wait 15+ minutes and perform an action → should still work without a
visible interruption (confirms the axios 401 → refresh → retry
interceptor); you can also shortcut this by temporarily setting
JWT_ACCESS_EXPIRES_IN=20sin the backend.envto trigger it quickly.
Verified here:
- Every backend file passes
node --check(syntax) and loads without import errors. - The position/slug helper functions are unit-tested inline (fractional indexing math, slug format).
- The full server boots, and I hit it live:
/api/v1/health→ 200, an unknown route → 404 via the error handler,/api/v1/auth/mewithout a token → 401 viaprotectmiddleware, and the Socket.io handshake endpoint responds correctly. - The full frontend builds cleanly via
vite build(1801 modules transformed, no errors) both before and after the Step 3 additions.
Not verified here (needs your machine): anything requiring a live MongoDB connection — actual signup/login persistence, workspace/board/task CRUD against real data, and the full drag-and-drop → socket → DB → other client round trip. This sandbox has no network path to a Mongo server or binary, so run through the manual test script above once you've got Mongo connected — that's the real end-to-end confirmation.
taskforge-backend/ Express + Mongoose + Socket.io
├── src/models/ User, Workspace, Board, List, Task
├── src/controllers/ auth, workspace, board, task
├── src/middleware/ auth (JWT), resourceAuth (membership checks)
├── src/socket/ Socket.io auth + task:move real-time handler
└── src/utils/position.js fractional indexing shared by drag-and-drop
taskforge-client/ React (Vite) + Tailwind + shadcn-style UI
├── src/api/ axios instance (token refresh), REST wrappers
├── src/socket/ socket.io-client connection + board-room hook
├── src/context/AuthContext session bootstrap, login/register/logout
├── src/pages/ Login, Register, Workspaces, WorkspaceBoards, Board
└── src/components/ Column, TaskCard, dialogs, shadcn ui/ primitives
-
Card creation and edits go through REST — infrequent, form-driven actions. The controller persists to Mongo, then does
io.to(boardRoom).emit('task:created' | 'task:updated' | 'task:deleted', ...)so every other open tab on that board updates without polling. -
Card movement (drag-and-drop) is the one thing that goes through Socket.io directly, not REST —
task:moveis emitted the instant a drag ends, the server persists the newlist/position, and broadcaststask:movedto everyone else in the room. The mover's own UI already updated optimistically, so the server excludes them (socket.to, notio.to) and rolls the optimistic change back only if the server rejects the move. -
Both list and task ordering use fractional indexing (
positionas a float, new position = average of its neighbours) so a reorder is a single-document write regardless of how many cards are on the board — seesrc/utils/position.js(backend) andsrc/lib/position.js(frontend, same math, used for the optimistic update). -
The access token lives in memory only and is passed to the socket handshake as
socket.handshake.auth.token; if it's expired when the socket tries to connect, the client silently calls/auth/refreshand reconnects once, mirroring the REST 401-retry interceptor.