A production-grade, real-time device tracking system built on WebSockets, Event-Driven Architecture, and full-stack Node.js engineering. Track multiple users simultaneously on a live interactive map with millisecond-precision location updates.
Built by Saurabh Singh Rajput β Self-Taught Full-Stack Developer & Coder Β· IIIT Bhagalpur
- π Live Demo
- π― What This Project Does
- ποΈ System Architecture
- β‘ Core Backend Concepts
- ποΈ Project Structure
- π» Tech Stack Deep Dive
- π API & Socket Event Reference
- π Complete Data Flow
- π§ Frontend Intelligence
- βοΈ Quick Start
- π Deployment
- πΊοΈ Roadmap
- π€ Contributing
β οΈ Cold Start Notice: Hosted on Render's free tier β the first request may take 5β15 seconds to wake the server. This is normal for free-tier cold starts.
π Grant location permission when prompted to see your GPS pin on the live map.
Testing Multi-User Tracking:
- Open the link in two different browser tabs or devices
- Watch both pins appear simultaneously on the map
- Move around β pins update in real-time via WebSocket
Nexus Tracker solves the classic real-time communication problem: how do you push live data from multiple clients to all other clients simultaneously β without any client needing to poll or ask?
| Feature | Description |
|---|---|
| π Live GPS Tracking | Captures latitude / longitude via browser Geolocation API |
| π₯ Multi-User Sync | Every connected user sees every other user's live position |
| π‘ Persistent WebSocket | Single bi-directional connection per client β zero polling overhead |
| π§ Smart Fallbacks | GPS β Retry β IP Geolocation 3-tier fallback chain |
| π Auto Cleanup | Disconnected user markers removed from all active maps instantly |
| πΊοΈ Interactive Map | Leaflet.js + OpenStreetMap with dark-mode filter & smooth animations |
| π± Responsive | Mobile-first design that works on any screen size |
graph TB
subgraph ClientA["π₯οΈ Client A β Browser"]
GPS_A["π Geolocation API"]
SOCKET_A["π‘ Socket.IO Client"]
MAP_A["πΊοΈ Leaflet.js Map"]
end
subgraph Server["βοΈ Node.js Server β Port 3000"]
EXPRESS["π Express.js\nHTTP Router"]
EJS["π EJS Template\nEngine"]
SOCKETIO["π Socket.IO\nWebSocket Server"]
BROADCAST["π’ io.emit()\nBroadcast Engine"]
end
subgraph ClientB["π₯οΈ Client B β Browser"]
GPS_B["π Geolocation API"]
SOCKET_B["π‘ Socket.IO Client"]
MAP_B["πΊοΈ Leaflet.js Map"]
end
GPS_A -->|"send-location\n{lat, lng}"| SOCKET_A
SOCKET_A -->|"WebSocket\nws://"| SOCKETIO
GPS_B -->|"send-location\n{lat, lng}"| SOCKET_B
SOCKET_B -->|"WebSocket\nws://"| SOCKETIO
SOCKETIO --> BROADCAST
BROADCAST -->|"receive-location\n{id, lat, lng}"| SOCKET_A
BROADCAST -->|"receive-location\n{id, lat, lng}"| SOCKET_B
SOCKET_A --> MAP_A
SOCKET_B --> MAP_B
EXPRESS -->|"GET /"| EJS
EJS -->|"Rendered HTML"| ClientA
EJS -->|"Rendered HTML"| ClientB
style Server fill:#1e293b,color:#38bdf8,stroke:#38bdf8
style ClientA fill:#0f172a,color:#94a3b8,stroke:#334155
style ClientB fill:#0f172a,color:#94a3b8,stroke:#334155
style BROADCAST fill:#7c3aed,color:#fff,stroke:#7c3aed
Architecture Pattern: Event-Driven Real-Time Pub/Sub The server acts as a message broker β receiving location events from any client and instantly broadcasting them to all connected clients. This is the same foundational pattern behind Google Maps Live Share, Uber driver tracking, and multiplayer games.
This project uses WebSockets β a critical architectural distinction from traditional HTTP:
| Dimension | HTTP (Traditional) | WebSocket (This Project) |
|---|---|---|
| Connection | Open β Request β Response β Close | Open once, stays open permanently |
| Direction | Client β Server only | Bi-directional (full-duplex) |
| Overhead | Full HTTP headers on every request | Minimal framing after handshake |
| Latency | High β new TCP handshake each time | Very low β persistent TCP connection |
| Real-time | Faked via setInterval polling |
Native push β instant delivery |
| Best For | REST APIs, static pages | Live tracking, chat, gaming |
WebSocket Handshake Flow:
sequenceDiagram
participant C as π₯οΈ Client Browser
participant S as βοΈ Node.js Server
C->>S: HTTP GET / (Accept: text/html)
S-->>C: 200 OK β EJS rendered HTML
Note over C,S: Script loads, socket.io client initializes
C->>S: HTTP GET /socket.io/?... (Upgrade: websocket)
S-->>C: 101 Switching Protocols
Note over C,S: β
Persistent WebSocket tunnel established
loop Every GPS update (~5β10s)
C->>S: send-location {latitude, longitude}
S-->>C: receive-location {id, lat, lng}
end
C->>S: disconnect (tab closed / network drop)
S-->>C: user-disconnected (broadcast to all)
Socket.IO adds on top of raw WebSockets:
- π Auto-reconnection on network drops
- π Fallback to HTTP long-polling if WebSockets are firewall-blocked
- π Room/namespace management for grouped broadcasts
- π Multiplexing multiple logical channels over one connection
The backend is built entirely on Node.js's event loop β single-threaded, non-blocking, handling thousands of concurrent sockets without spawning threads.
// β
EVENT-DRIVEN PATTERN β Non-blocking, scalable
io.on('connection', (socket) => { // π Event: new client joined
socket.on('send-location', (data) => { // π Event: GPS update received
io.emit('receive-location', {
id: socket.id,
...data // Spread: merges id + {lat, lng}
});
});
socket.on('disconnect', () => { // β Event: client left
io.emit('user-disconnected', socket.id);
});
});Emit Method Comparison:
| Method | Reaches | Use Case |
|---|---|---|
io.emit(event, data) |
ALL connected sockets | Broadcast GPS to everyone |
socket.emit(event, data) |
Only that one client | Send private acknowledgement |
socket.broadcast.emit(...) |
Everyone except sender | Notify others of your presence |
io.to(room).emit(...) |
All in a room | Group/fleet tracking (future) |
flowchart LR
A["π₯οΈ Client A\nGPS Update\nlat: 28.6, lng: 77.2"]
S["βοΈ Server\nio.emit()"]
R1["π₯οΈ Client A\nβ
Own marker updated"]
R2["π₯οΈ Client B\nπ A's marker appears"]
R3["π₯οΈ Client C\nπ A's marker appears"]
A -->|"send-location\n{lat, lng}"| S
S -->|"receive-location\n{id:'aB3x', lat, lng}"| R1
S -->|"receive-location\n{id:'aB3x', lat, lng}"| R2
S -->|"receive-location\n{id:'aB3x', lat, lng}"| R3
style S fill:#7c3aed,color:#fff
style A fill:#0f172a,color:#38bdf8,stroke:#38bdf8
style R1 fill:#064e3b,color:#6ee7b7,stroke:#059669
style R2 fill:#1e293b,color:#94a3b8,stroke:#334155
style R3 fill:#1e293b,color:#94a3b8,stroke:#334155
Why
io.emit()and notsocket.broadcast.emit()? Usingio.emit()means Client A also receives its own location echo. This ensures A's own marker renders consistently across all sessions β a self-including broadcast.
The Spread Operator Pattern:
io.emit('receive-location', { id: socket.id, ...data });
// ββββββββββββββββββββββββ
// Merges: { id: "aBcDeFg1x", latitude: 28.6, longitude: 77.2 }
// The client uses `id` as the map marker dictionary keystateDiagram-v2
[*] --> Connecting : Browser opens page
Connecting --> Connected : WebSocket handshake β
\nsocket.id assigned
Connected --> Tracking : socket.on('send-location')\nGPS data received
Tracking --> Tracking : io.emit('receive-location')\nBroadcast to ALL clients
Connected --> Disconnected : Tab closed / network drop\nsocket.on('disconnect')
Tracking --> Disconnected : Tab closed / network drop
Disconnected --> Cleanup : io.emit('user-disconnected', socket.id)\nAll clients remove marker
Cleanup --> [*] : markers[id] deleted\nmap.removeLayer() called
Key Design Decisions:
| Decision | Why |
|---|---|
Use socket.id as device ID |
Auto-generated, unique per session, no DB required |
| Ephemeral IDs | Reconnection creates new ID β keeps architecture stateless |
markers[socket.id] as key |
O(1) lookup and deletion when disconnection event fires |
| Server emits disconnect event | Clients have no way to know a peer left β server is the source of truth |
tracking-device/
β
βββ π app.js β Entry point: Express + Socket.IO server
β
βββ π views/
β βββ π index.ejs β SSR HTML template (EJS view engine)
β
βββ π public/ β Static assets auto-served by Express
β βββ π css/
β β βββ π¨ style.css β Glassmorphism dark UI, responsive layout
β βββ π js/
β βββ β‘ script.js β Client: GPS, Socket.IO, Leaflet logic
β
βββ π package.json β Project manifest & npm dependencies
βββ π package-lock.json β Exact dependency version lockfile
βββ π .gitignore β Excludes node_modules, .env
Layer Responsibilities:
graph LR
A["π app.js\nSERVER LAYER"] -->|"renders"| B["π index.ejs\nVIEW LAYER"]
A -->|"serves"| C["π¨ style.css\nSTYLE LAYER"]
A -->|"serves"| D["β‘ script.js\nCLIENT LOGIC LAYER"]
D -->|"emits events"| A
style A fill:#7c3aed,color:#fff,stroke:#7c3aed
style B fill:#1e40af,color:#bfdbfe,stroke:#3b82f6
style C fill:#065f46,color:#a7f3d0,stroke:#059669
style D fill:#92400e,color:#fde68a,stroke:#d97706
graph TD
subgraph Backend["βοΈ Backend β Node.js Runtime"]
NJS["Node.js v18+\nV8 Engine Β· Event Loop"]
EXP["Express.js v5\nHTTP Routing Β· Middleware"]
SIO["Socket.IO v4.8\nWebSocket Server"]
EJS2["EJS v3.1\nServer-Side Templating"]
end
subgraph Frontend["π Frontend β Browser"]
SIO_C["Socket.IO Client\nWebSocket Client"]
LEAF["Leaflet.js v1.9.4\nMap Rendering"]
GEO["Geolocation API\nGPS / Network"]
CSS["Custom CSS\nGlassmorphism Β· Dark UI"]
end
subgraph Infra["π Infrastructure"]
RENDER["Render.com\nFree Tier PaaS"]
OSM["OpenStreetMap\nTile CDN (free)"]
IPAPI["ipapi.co\nIP Geolocation Fallback"]
end
NJS --> EXP
NJS --> SIO
EXP --> EJS2
SIO <-->|"WebSocket"| SIO_C
SIO_C --> LEAF
GEO --> SIO_C
LEAF --> OSM
EXP --> RENDER
GEO -.->|"fallback"| IPAPI
style Backend fill:#1e293b,color:#38bdf8,stroke:#38bdf8
style Frontend fill:#0f172a,color:#94a3b8,stroke:#334155
style Infra fill:#1a1a2e,color:#a78bfa,stroke:#7c3aed
Node.js executes JavaScript outside the browser via V8 (Chrome's JS engine). Its non-blocking I/O and event loop allow a single process to handle thousands of concurrent WebSocket connections without spawning threads β each client is just an event listener in memory.
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Middleware: serve /public as static root
app.use(express.static(path.join(__dirname, 'public')));
// Route: render EJS view on GET /
app.get('/', (req, res) => res.render('index'));| Responsibility | How |
|---|---|
| Routing | app.get('/') maps the root route to EJS render |
| Static Serving | express.static('public') auto-serves CSS, JS files |
| Middleware Pipeline | Runs express.static before route handlers |
| View Engine | app.set('view engine', 'ejs') registers EJS |
const http = require('http');
const { Server } = require('socket.io');
// β οΈ KEY: Socket.IO needs the raw http.Server, NOT the Express app
const server = http.createServer(app); // Wraps Express in Node's http module
const io = new Server(server); // Attaches Socket.IO to same portWhy
http.createServer(app)and not justapp.listen()? Socket.IO must intercept the raw HTTPUpgradeheader at the TCP level β before Express processes the request. Sharing onehttp.Serverinstance lets both Express (HTTP) and Socket.IO (WebSocket) listen on port 3000 simultaneously.
On GET /, Express compiles views/index.ejs into plain HTML and sends it:
| EJS Syntax | Purpose |
|---|---|
<%= variable %> |
Output escaped HTML value |
<% code %> |
Execute JS logic inline (loops, conditions) |
<%- include('partial') %> |
Compose sub-templates |
const map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19 // Street-level precision
}).addTo(map);Uses OpenStreetMap tiles β completely free, no API key required.
// watchPosition fires continuously on device movement
navigator.geolocation.watchPosition(
({ coords }) => {
socket.emit('send-location', {
latitude: coords.latitude,
longitude: coords.longitude
});
},
errorHandler,
{ enableHighAccuracy: true, timeout: 20000, maximumAge: 10000 }
);
watchPositionvsgetCurrentPosition:getCurrentPositionfires once.watchPositionfires every time the device moves β essential for continuous live tracking.
| Method | Route | Content-Type | Description |
|---|---|---|---|
GET |
/ |
text/html |
EJS-rendered tracking UI |
GET |
/css/style.css |
text/css |
Dark theme stylesheet |
GET |
/js/script.js |
application/javascript |
GPS + map client script |
| Event | Payload Type | Payload Fields | When Emitted |
|---|---|---|---|
send-location |
Object |
{ latitude: Number, longitude: Number } |
Every GPS position update |
| Event | Payload Type | Payload Fields | When Emitted |
|---|---|---|---|
receive-location |
Object |
{ id: String, latitude: Number, longitude: Number } |
Any client sends location |
user-disconnected |
String |
socketId |
A client disconnects |
Full Interaction Sequence:
sequenceDiagram
participant A as π₯οΈ Client A
participant S as βοΈ Server
participant B as π₯οΈ Client B
Note over A,B: Both clients connected via WebSocket
A->>S: send-location {lat: 28.6, lng: 77.2}
S-->>A: receive-location {id:"aB3x", lat:28.6, lng:77.2}
S-->>B: receive-location {id:"aB3x", lat:28.6, lng:77.2}
Note over A: Moves own π’ marker
Note over B: Creates/moves π΅ marker for A
B->>S: send-location {lat: 19.0, lng: 72.8}
S-->>A: receive-location {id:"xY9z", lat:19.0, lng:72.8}
S-->>B: receive-location {id:"xY9z", lat:19.0, lng:72.8}
Note over A: Creates/moves π΅ marker for B
Note over B: Moves own π’ marker
Note over A: User closes browser tab
A->>S: disconnect (automatic)
S-->>B: user-disconnected "aB3x"
Note over B: Removes A's π΅ marker from map
flowchart TD
START(["π User opens browser"]) --> HTTP["HTTP GET /\nExpress router"]
HTTP --> EJS_RENDER["EJS compiles index.ejs\nβ HTML response"]
EJS_RENDER --> LOAD["Page loads in browser\nscript.js executes"]
LOAD --> WS_CONNECT["socket.io client\nWebSocket handshake\nβ socket.id assigned"]
LOAD --> MAP_INIT["Leaflet.js\nmap initialized\nsetView 0,0 zoom:2"]
LOAD --> GEO_WATCH["navigator.geolocation\n.watchPosition() starts"]
GEO_WATCH --> GPS_SUCCESS{"GPS signal\nobtained?"}
GPS_SUCCESS -->|"β
Yes"| EMIT["socket.emit\n'send-location'\n{lat, lng}"]
GPS_SUCCESS -->|"β Timeout"| RETRY["Retry with\nlow accuracy"]
RETRY --> GPS_SUCCESS
RETRY -->|"3 retries failed"| IP_FALLBACK["fetch ipapi.co/json\nIP Geolocation"]
IP_FALLBACK --> EMIT
EMIT --> SERVER_RECV["Server receives\nsocket.on('send-location')"]
SERVER_RECV --> BROADCAST["io.emit('receive-location')\n{id, lat, lng}\nβ ALL clients"]
BROADCAST --> SELF["Client A\nUpdate own marker π’"]
BROADCAST --> OTHERS["Client B, C...\nCreate/move marker π΅"]
WS_CONNECT --> DISCONNECT{"Client\ndisconnects?"}
DISCONNECT -->|"Yes"| SERVER_DISC["Server: socket.on('disconnect')"]
SERVER_DISC --> DISC_BROADCAST["io.emit('user-disconnected'\nsocket.id)"]
DISC_BROADCAST --> CLEANUP["All clients:\nmap.removeLayer(marker)\ndelete markers[id]"]
CLEANUP --> DONE(["β
Map cleaned up"])
style START fill:#7c3aed,color:#fff,stroke:#7c3aed
style BROADCAST fill:#7c3aed,color:#fff,stroke:#7c3aed
style EMIT fill:#1e40af,color:#bfdbfe,stroke:#3b82f6
style GPS_SUCCESS fill:#1e293b,color:#94a3b8,stroke:#334155
style IP_FALLBACK fill:#92400e,color:#fde68a,stroke:#d97706
style CLEANUP fill:#064e3b,color:#a7f3d0,stroke:#059669
style DONE fill:#064e3b,color:#a7f3d0,stroke:#059669
flowchart TD
START(["π Page loads"]) --> T1["Tier 1: High-Accuracy GPS\nenableHighAccuracy: true\ntimeout: 30s"]
T1 --> T1_OK{"Success?"}
T1_OK -->|"β
GPS Lock"| USE["Use coordinates\nShow π’ green badge\nZoom map to level 16"]
T1_OK -->|"β Timeout"| T2["Tier 2: Low-Accuracy GPS\nenableHighAccuracy: false\nFaster lock, less precise"]
T2 --> T2_OK{"Success?"}
T2_OK -->|"β
GPS Lock"| USE
T2_OK -->|"β Retry 1/3"| T2
T2_OK -->|"β All 3 retries failed"| T3["Tier 3: IP Geolocation\nfetch ipapi.co/json\nCity-level accuracy ~1β10km"]
T3 --> T3_OK{"Success?"}
T3_OK -->|"β
IP data received"| IP_USE["Use IP coordinates\nShow π‘ yellow badge\nZoom map to level 12"]
T3_OK -->|"β API error"| ERR["Show β error alert\nPrompt to enable GPS"]
style START fill:#7c3aed,color:#fff
style USE fill:#064e3b,color:#a7f3d0,stroke:#059669
style IP_USE fill:#78350f,color:#fde68a,stroke:#d97706
style ERR fill:#7f1d1d,color:#fca5a5,stroke:#ef4444
style T1 fill:#1e293b,color:#38bdf8,stroke:#0284c7
style T2 fill:#1e293b,color:#94a3b8,stroke:#334155
style T3 fill:#1e293b,color:#a78bfa,stroke:#7c3aed
// Only recenter if device moved MORE than 100 meters
// Prevents map jitter from GPS accuracy oscillation (typically 5β50m)
const distance = map.distance(
[currentCenter.lat, currentCenter.lng],
[latitude, longitude]
);
if (distance > 100) {
map.setView([latitude, longitude], 16, {
animate: true,
duration: 0.5,
easeLinearity: 0.25
});
}// ownId is stored on socket 'connect' event
socket.on('connect', () => { ownId = socket.id; });
// Marker color is determined purely client-side β zero server changes needed
icon: L.icon({
iconUrl: id === ownId
? 'marker-icon-2x-green.png' // π’ Your own device
: 'marker-icon-2x-blue.png' // π΅ Other users
})| Badge Text | Badge Color | Triggered By |
|---|---|---|
Initializing GPS... |
π΅ Sky Blue | Page just loaded |
GPS Active (Xm) |
π’ Green | GPS lock β accuracy in meters shown |
GPS Timeout (Retry N/3) |
π‘ Amber | Retrying GPS acquisition |
IP Location (Less Precise) |
π‘ Amber | IP geolocation fallback active |
Location Access Denied |
π΄ Red | User denied browser permission |
GPS Unavailable |
π΄ Red | Hardware or signal issue |
- Node.js
v18+β Download - Modern browser: Chrome, Edge, Firefox, Safari
- Location permissions enabled in browser settings
# Clone the repository
git clone https://github.com/DevSars24/tracking-device.git
# Navigate into project
cd tracking-device
# Install all dependencies
npm installnode app.js
# β Server running on http://localhost:3000Visit http://localhost:3000 Β· Grant location access Β· Open in a second tab to test multi-user tracking.
{
"express": "^5.1.0", β HTTP framework, routing, static serving
"socket.io": "^4.8.1", β WebSocket server with long-poll fallback
"ejs": "^3.1.10" β Server-side HTML template engine
}Currently deployed on Render free tier.
- Push code to GitHub
- Go to render.com β New Web Service
- Connect your repository
- Set build config:
Runtime: Node Build Command: npm install Start Command: node app.js - Click Deploy β Render assigns a public
*.onrender.comURL
// β Current (local only)
server.listen(3000, () => console.log('Server running on http://localhost:3000'));
// β
Production-ready (dynamic port for Render/Heroku/Railway)
server.listen(process.env.PORT || 3000, () => {
console.log(`Server running on port ${process.env.PORT || 3000}`);
});| Environment | Port | Notes |
|---|---|---|
| Local Dev | 3000 |
Hardcoded, works immediately |
| Render / Heroku | process.env.PORT |
Assigned by platform at runtime |
gantt
title Nexus Tracker β Feature Roadmap
dateFormat YYYY-MM
section Shipped
Real-time GPS tracking :done, 2025-01, 2025-02
Multi-user WebSocket sync :done, 2025-02, 2025-03
Dark mode map UI :done, 2025-03, 2025-04
GPS + IP fallback chain :done, 2025-04, 2025-05
section Planned
JWT auth & named users :active, 2025-06, 2025-08
Private room tracking :2025-08, 2025-10
MongoDB location history :2025-10, 2025-12
React Native mobile app :2026-01, 2026-06
Redis pub/sub scaling :2026-06, 2026-09
| Version | Feature | Status |
|---|---|---|
| v1.0 | Real-time multi-user tracking, GPS + IP fallback, dark UI | β Live |
| v2.0 | JWT authentication, named users, persistent sessions | π Planned |
| v2.1 | Room-based private group tracking, Mapbox directions | π Planned |
| v2.2 | MongoDB location history, heatmaps, route replay | π Planned |
| v3.0 | React Native mobile app, push notifications | π Planned |
| v3.1 | Redis pub/sub for horizontal server scaling | π Planned |
# 1. Fork on GitHub & clone your fork
git clone https://github.com/YOUR_USERNAME/tracking-device.git
# 2. Create a feature branch
git checkout -b feature/your-feature-name
# 3. Commit with convention
git commit -m "feat: what you added"
# 4. Push & open a Pull Request
git push origin feature/your-feature-nameCommit message convention:
| Prefix | Purpose |
|---|---|
feat: |
New feature |
fix: |
Bug fix |
docs: |
Documentation update |
style: |
CSS / UI change |
refactor: |
Code restructure (no behavior change) |
perf: |
Performance improvement |
Licensed under the ISC License β see LICENSE for full text.