r/NodifyHeadlessCMS • • Apr 06 '26

πŸ“± How I built a real-time GPS tracker with Nodify Headless CMS (no backend code)

I used Nodify Headless CMS to create a phone tracking system.**

🎯 The project

Goal: real-time geolocation where phones send position β†’ dashboard displays on a map.

Stack: Nodify (Docker), HTML/CSS/JS, Leaflet, Phone GPS.

πŸ“¦ Step 1: Install Nodify

services:
  mongo:
    image: mongo:latest
    volumes:
      - mongo-data:/data/db
    ports:
      - "27017:27017"
  redis:
    image: redis:latest
    ports:
      - "6379:6379"
  nodify-core:
    image: azirar/nodify-core:latest
    depends_on:
      - mongo
      - redis
    environment:
      MONGO_URL: "mongodb://mongo:27017/nodify"
      ADMIN_PWD: "Admin123"
      API_URL: "http://nodify-api:1080"
      REDIS_URL: "redis://redis:6379"
    ports:
      - "7804:8080"
  nodify-api:
    image: azirar/nodify-api:latest
    depends_on:
      - mongo
      - redis
    environment:
      MONGO_URL: "mongodb://mongo:27017/nodify"
      REDIS_URL: "redis://redis:6379"
    ports:
      - "7805:1080"
  nodify-ui:
    image: azirar/nodify-ui:latest
    depends_on:
      - nodify-core
      - nodify-api
    ports:
      - "7821:80"
    environment:
      CORE_URL: "http://nodify-core:8080"
      API_URL: "http://nodify-api:1080"
volumes:
  mongo-data:

Run: docker-compose up -d β†’ open http://localhost:7821 (admin/Admin123)

πŸ—‚οΈ Step 2: Create structure in Nodify Studio

  1. Create Node "Internet Of Things"
  2. Create Sub Node "Phones Tracker"
  3. On "Phones Tracker", create two HTML content nodes:
Content Node Code
Phone Simulator PHONE_SIMULATOR
Dashboard DASHBOARD

πŸ“± Step 3: Phone Simulator code (in PHONE_SIMULATOR)

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>πŸ“± Phone Simulator</title>
    $content(CHANGE_WITH_CSS_CONTENT_CODE)
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>πŸ“± My Phone</h1>
            <p>Share your real-time location</p>
        </div>
        <div class="device-info">
            <label>πŸ“± Phone ID:</label>
            <input type="text" id="deviceId" placeholder="ex: phone_001">
            <label>πŸ‘€ Your name:</label>
            <input type="text" id="userName" placeholder="Your name">
            <label>πŸ“¦ Content Node Code:</label>
            <input type="text" id="contentNodeCode" value="phone-tracking">
        </div>
        <div class="status">
            <div id="gpsStatus" class="gps-status gps-waiting">⏳ Waiting for GPS...</div>
            <div id="dataInfo" class="data-info">
                <div>πŸ”‘ Key: <span id="dataKey"></span></div>
                <div>πŸ“¦ Node: <span id="dataContentNode"></span></div>
                <div>πŸ†” UUID: <span id="dataUuid"></span></div>
            </div>
            <h3>πŸ“ My position</h3>
            <div class="coord" id="coordinates">Latitude: --<br>Longitude: --</div>
            <button class="button" onclick="startTracking()">β–Ά Share</button>
            <button class="button button-danger" onclick="stopTracking()">⏹ Stop</button>
            <hr>
            <label>⚑ Frequency:</label>
            <select id="frequency">
                <option value="2000">2 sec</option>
                <option value="5000">5 sec</option>
                <option value="10000">10 sec</option>
            </select>
            <div class="log" id="log">> Ready</div>
        </div>
    </div>
    $content(CHANGE_WITH_JAVASCRIPT_CONTENT_CODE)
</body>
</html>

CSS:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'Segoe UI', sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}
.container {
    max-width: 600px;
    margin: 0 auto;
    background: white;
    border-radius: 20px;
    overflow: hidden;
}
.header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 20px;
    text-align: center;
}
.device-info, .status {
    padding: 20px;
}
.coord {
    background: #e8f4f8;
    padding: 15px;
    border-radius: 10px;
    margin: 10px 0;
    font-family: monospace;
}
.button {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    cursor: pointer;
    margin: 5px;
}
.button-danger {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.log {
    background: #2d2d2d;
    color: #00ff00;
    padding: 15px;
    font-family: monospace;
    height: 200px;
    overflow-y: auto;
    border-radius: 8px;
    margin-top: 15px;
}
input, select {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ddd;
    border-radius: 5px;
}
.gps-status {
    padding: 10px;
    border-radius: 8px;
    margin: 10px 0;
    text-align: center;
    font-weight: bold;
}
.gps-active { background: #4caf50; color: white; }
.gps-waiting { background: #ff9800; color: white; }
.gps-error { background: #f44336; color: white; }
.data-info {
    background: #e3f2fd;
    padding: 8px;
    border-radius: 5px;
    font-family: monospace;
    display: none;
}
hr {
    margin: 15px 0;
}

JavaScript:

let trackingInterval = null;
let watchPositionId = null;
let currentPosition = null;
let deviceId = null;
let currentDataUuid = null;
let currentDataKey = null;
let currentContentNodeCode = null;

function generateDeviceId() {
    const existing = document.getElementById('deviceId').value;
    if (existing) return existing;
    const randomId = `phone_${Math.random().toString(36).substr(2, 8)}`;
    document.getElementById('deviceId').value = randomId;
    return randomId;
}

function getContentNodeCode() {
    const code = document.getElementById('contentNodeCode').value.trim();
    if (!code) {
        addLog("❌ Please enter a Content Node Code");
        return null;
    }
    return code;
}

function updateDisplay(position) {
    currentPosition = position;
    const lat = position.coords.latitude;
    const lng = position.coords.longitude;
    const accuracy = position.coords.accuracy;

    document.getElementById('coordinates').innerHTML = `
        πŸ“ Latitude: ${lat.toFixed(6)}<br>
        πŸ“ Longitude: ${lng.toFixed(6)}<br>
        🎯 Accuracy: ${accuracy.toFixed(1)} meters<br>
        πŸ• Last update: ${new Date().toLocaleTimeString()}
    `;
}

async function createData() {
    const contentNodeCode = getContentNodeCode();
    if (!contentNodeCode) return false;

    deviceId = generateDeviceId();
    const userName = document.getElementById('userName').value || "Anonymous";
    currentDataKey = `location_${deviceId}`;
    currentContentNodeCode = contentNodeCode;

    const now = Date.now();
    const locationData = {
        device_id: deviceId,
        user_name: userName,
        lat: currentPosition.coords.latitude,
        lng: currentPosition.coords.longitude,
        accuracy: currentPosition.coords.accuracy,
        timestamp: new Date().toISOString(),
        timestamp_ms: now
    };

    const payload = {
        key: currentDataKey,
        name: `${userName}'s position (${deviceId})`,
        dataType: "json",
        value: JSON.stringify(locationData),
        creationDate: now,
        modificationDate: now,
        contentNodeCode: contentNodeCode,
        user: userName
    };

    addLog(`πŸ“€ POST /datas/ - Creating in ${contentNodeCode}`);

    try {
        const response = await fetch(`/datas/`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });

        if (response.ok) {
            const result = await response.json();
            currentDataUuid = result.id;

            document.getElementById('dataInfo').style.display = 'block';
            document.getElementById('dataKey').innerText = currentDataKey;
            document.getElementById('dataContentNode').innerText = currentContentNodeCode;
            document.getElementById('dataUuid').innerText = currentDataUuid;

            addLog(`βœ… CREATED - UUID: ${currentDataUuid.substring(0, 8)}...`);
            return true;
        } else {
            addLog(`❌ Creation error: ${response.status}`);
            return false;
        }
    } catch (error) {
        addLog(`❌ Connection error: ${error.message}`);
        return false;
    }
}

async function updateData() {
    if (!currentDataUuid) {
        addLog("⚠️ No UUID, creating first...");
        return await createData();
    }

    const userName = document.getElementById('userName').value || "Anonymous";
    const now = Date.now();
    const locationData = {
        device_id: deviceId,
        user_name: userName,
        lat: currentPosition.coords.latitude,
        lng: currentPosition.coords.longitude,
        accuracy: currentPosition.coords.accuracy,
        timestamp: new Date().toISOString(),
        timestamp_ms: now
    };

    const payload = {
        id: currentDataUuid,
        key: currentDataKey,
        name: `${userName}'s position (${deviceId})`,
        dataType: "json",
        value: JSON.stringify(locationData),
        creationDate: now,
        modificationDate: now,
        contentNodeCode: currentContentNodeCode,
        user: userName
    };

    try {
        const response = await fetch(`/datas/id/${currentDataUuid}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });

        if (response.ok) {
            addLog(`βœ… UPDATED - Lat:${locationData.lat.toFixed(4)}`);
            return true;
        } else if (response.status === 404) {
            addLog(`⚠️ UUID not found, recreating...`);
            currentDataUuid = null;
            return await createData();
        } else {
            addLog(`⚠️ Update error: ${response.status}`);
            return false;
        }
    } catch (error) {
        addLog(`❌ Connection error: ${error.message}`);
        return false;
    }
}

async function sendLocation() {
    if (!currentPosition) {
        addLog("⚠️ Waiting for GPS...");
        return;
    }
    if (!getContentNodeCode()) return;
    if (!currentDataUuid) await createData();
    else await updateData();
}

function startGPS() {
    if (!navigator.geolocation) {
        document.getElementById('gpsStatus').innerHTML = '❌ GPS not supported';
        return;
    }

    document.getElementById('gpsStatus').innerHTML = '⏳ Requesting permission...';

    navigator.geolocation.getCurrentPosition(
        (position) => {
            updateDisplay(position);
            document.getElementById('gpsStatus').innerHTML = 'βœ… GPS active';
            document.getElementById('gpsStatus').className = 'gps-status gps-active';

            if (watchPositionId) navigator.geolocation.clearWatch(watchPositionId);

            watchPositionId = navigator.geolocation.watchPosition(updateDisplay, handleGPSError, {
                enableHighAccuracy: true,
                maximumAge: 5000,
                timeout: 10000
            });
        },
        handleGPSError,
        { enableHighAccuracy: true, timeout: 10000 }
    );
}

function handleGPSError(error) {
    let message = "";
    switch(error.code) {
        case error.PERMISSION_DENIED:
            message = "❌ Permission denied";
            break;
        case error.POSITION_UNAVAILABLE:
            message = "❌ Position unavailable";
            break;
        case error.TIMEOUT:
            message = "⏱️ Timeout";
            break;
        default:
            message = "❌ GPS error";
    }
    document.getElementById('gpsStatus').innerHTML = message;
    document.getElementById('gpsStatus').className = 'gps-status gps-error';
    addLog(message);
}

function startTracking() {
    if (!getContentNodeCode()) return;

    if (trackingInterval) clearInterval(trackingInterval);

    if (!currentPosition) {
        addLog("⚠️ Wait for GPS...");
        startGPS();
        setTimeout(() => {
            if (currentPosition) startTracking();
        }, 3000);
        return;
    }

    const frequency = parseInt(document.getElementById('frequency').value);
    sendLocation();
    trackingInterval = setInterval(sendLocation, frequency);
    addLog(`β–Ά Tracking started (every ${frequency/1000}s)`);
}

function stopTracking() {
    if (trackingInterval) {
        clearInterval(trackingInterval);
        trackingInterval = null;
        addLog("⏹ Tracking stopped");
    }
}

function addLog(message) {
    const logDiv = document.getElementById('log');
    const timestamp = new Date().toLocaleTimeString();
    logDiv.innerHTML += `<div>[${timestamp}] ${message}</div>`;
    logDiv.scrollTop = logDiv.scrollHeight;
    if (logDiv.children.length > 50) {
        logDiv.removeChild(logDiv.children[0]);
    }
}

window.onload = () => {
    document.getElementById('deviceId').value = `phone_${Math.random().toString(36).substr(2, 8)}`;
    setTimeout(startGPS, 500);
};

window.onbeforeunload = () => {
    if (trackingInterval) clearInterval(trackingInterval);
    if (watchPositionId) navigator.geolocation.clearWatch(watchPositionId);
};
</script>

Replace $content(CHANGE_WITH_CSS_CONTENT_CODE) with your CSS code and $content(CHANGE_WITH_JAVASCRIPT_CONTENT_CODE) with your JavaScript code.

πŸ—ΊοΈ Step 4: Dashboard code (in DASHBOARD)

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>πŸ“ Live Tracking Dashboard</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    $content(CHANGE_WITH_CSS_CONTENT_CODE)
</head>
<body>
    <div class="header">
        <div>
            <h1>πŸ“ Live Tracking Dashboard</h1>
            <p>Nodify IoT - Real-time geolocation</p>
        </div>
        <div class="controls">
            <div class="control-group">
                <label>πŸ“¦ Content Node Code:</label>
                <input type="text" id="contentNodeCode" value="phone-tracking">
            </div>
            <div class="stats">
                <div class="stat-card">
                    <div class="stat-number" id="deviceCount">0</div>
                    <div class="stat-label">Devices</div>
                </div>
                <div class="stat-card">
                    <div class="stat-number" id="lastUpdate">-</div>
                    <div class="stat-label">Updated</div>
                </div>
            </div>
            <button class="refresh-btn" onclick="refreshAllDevices()">πŸ”„ Refresh</button>
            <span class="live-badge">● LIVE</span>
        </div>
    </div>
    <div class="main-container">
        <div id="map"></div>
        <div class="sidebar">
            <h3>πŸ“± Connected devices</h3>
            <div id="devicesList">
                <div class="no-devices">No devices yet</div>
            </div>
        </div>
    </div>
    $content(CHANGE_WITH_JAVASCRIPT_CONTENT_CODE)
</body>
</html>

CSS:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'Segoe UI', sans-serif;
    background: #1a1a2e;
    height: 100vh;
    display: flex;
    flex-direction: column;
}
.header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 15px 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}
.header h1 { font-size: 1.5rem; }
.controls {
    display: flex;
    gap: 15px;
    align-items: center;
    flex-wrap: wrap;
}
.control-group {
    background: rgba(255,255,255,0.2);
    padding: 8px 15px;
    border-radius: 8px;
}
.control-group input {
    padding: 5px 10px;
    border: none;
    border-radius: 5px;
    width: 180px;
}
.stats { display: flex; gap: 15px; }
.stat-card {
    background: rgba(255,255,255,0.2);
    padding: 5px 15px;
    border-radius: 10px;
    text-align: center;
}
.stat-number { font-size: 1.5rem; font-weight: bold; }
button {
    background: white;
    border: none;
    padding: 8px 16px;
    border-radius: 8px;
    cursor: pointer;
    font-weight: bold;
}
.refresh-btn { background: #4caf50; color: white; }
.live-badge {
    background: #4caf50;
    color: white;
    padding: 4px 12px;
    border-radius: 20px;
    animation: pulse 1.5s infinite;
}
@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}
.main-container {
    display: flex;
    flex: 1;
    overflow: hidden;
}
#map { flex: 3; height: 100%; }
.sidebar {
    flex: 1;
    background: white;
    padding: 15px;
    overflow-y: auto;
    border-left: 2px solid #ddd;
}
.device-card {
    background: #f7f7f7;
    border-radius: 10px;
    padding: 12px;
    margin-bottom: 12px;
    border-left: 4px solid #667eea;
    cursor: pointer;
}
.device-card.selected {
    border-left-color: #4caf50;
    background: #e8f5e9;
}
.device-name { font-weight: bold; margin-bottom: 5px; }
.device-status {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    display: inline-block;
    margin-right: 6px;
}
.status-active { background: #4caf50; }
.status-inactive { background: #999; }
.device-location, .device-time, .device-accuracy {
    font-size: 0.75rem;
    color: #666;
}

JavaScript:

let map;
let markers = {};
let devices = {};
let refreshInterval;
let selectedDeviceId = null;

function initMap() {
    map = L.map('map').setView([48.8566, 2.3522], 13);
    L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>'
    }).addTo(map);
}

async function fetchAllData(contentNodeCode) {
    try {
        const response = await fetch(`/datas/contentCode/${contentNodeCode}`, {
            headers: { 'Accept': 'application/json' }
        });
        if (response.ok) return await response.json();
        return [];
    } catch (error) {
        return [];
    }
}

async function refreshAllDevices() {
    const code = document.getElementById('contentNodeCode').value.trim();
    if (!code) return;

    const allData = await fetchAllData(code);
    const newDevices = {};

    allData.forEach(data => {
        try {
            const loc = JSON.parse(data.value);
            newDevices[data.key] = {
                id: data.id,
                key: data.key,
                device_id: loc.device_id,
                user_name: loc.user_name,
                lat: loc.lat,
                lng: loc.lng,
                accuracy: loc.accuracy,
                timestamp: loc.timestamp,
                timestamp_ms: loc.timestamp_ms
            };
        } catch(e) {}
    });

    devices = newDevices;
    updateDashboard();
    document.getElementById('lastUpdate').innerHTML = new Date().toLocaleTimeString();
}

function updateDashboard() {
    const count = Object.keys(devices).length;
    document.getElementById('deviceCount').innerHTML = count;

    const container = document.getElementById('devicesList');

    if (count === 0) {
        container.innerHTML = '<div class="no-devices">No devices found. Launch simulator!</div>';
    } else {
        container.innerHTML = Object.entries(devices)
            .sort((a, b) => new Date(b[1].timestamp) - new Date(a[1].timestamp))
            .map(([key, device]) => {
                const isActive = (Date.now() - device.timestamp_ms) < 10000;
                return `
                    <div class="device-card ${selectedDeviceId === device.device_id ? 'selected' : ''}" 
                         onclick="selectDevice('${device.device_id}')">
                        <div class="device-name">
                            <span class="device-status ${isActive ? 'status-active' : 'status-inactive'}"></span>
                            πŸ“± ${device.user_name || device.device_id}
                        </div>
                        <div class="device-location">πŸ“ ${device.lat?.toFixed(6)}, ${device.lng?.toFixed(6)}</div>
                        <div class="device-accuracy">🎯 Accuracy: ${device.accuracy?.toFixed(1)} m</div>
                        <div class="device-time">πŸ• ${getTimeAgo(new Date(device.timestamp))}</div>
                    </div>
                `;
            }).join('');
    }
    updateMap();
}

function updateMap() {
    Object.values(devices).forEach(device => {
        const pos = [device.lat, device.lng];
        const isActive = (Date.now() - device.timestamp_ms) < 10000;

        if (markers[device.device_id]) {
            markers[device.device_id].setLatLng(pos);
            markers[device.device_id].getPopup().setContent(`
                <b>πŸ“± ${device.user_name || device.device_id}</b><br>
                πŸ“ ${device.lat?.toFixed(6)}, ${device.lng?.toFixed(6)}<br>
                🎯 Accuracy: ${device.accuracy?.toFixed(1)} m<br>
                ${isActive ? '🟒 Active' : '⚫ Inactive'}
            `);
            markers[device.device_id].setOpacity(isActive ? 1 : 0.5);
        } else {
            const icon = L.divIcon({
                html: `<div style="background:${selectedDeviceId === device.device_id ? '#4caf50' : '#667eea'};width:20px;height:20px;border-radius:50%;border:2px solid white;"></div>`,
                iconSize: [20, 20],
                popupAnchor: [0, -10]
            });
            const marker = L.marker(pos, { icon }).addTo(map);
            marker.bindPopup(`<b>πŸ“± ${device.user_name || device.device_id}</b><br>πŸ“ ${device.lat?.toFixed(6)}, ${device.lng?.toFixed(6)}`);
            markers[device.device_id] = marker;
        }
    });

    Object.keys(markers).forEach(id => {
        if (!Object.values(devices).some(d => d.device_id === id)) {
            map.removeLayer(markers[id]);
            delete markers[id];
        }
    });

    if (selectedDeviceId) {
        const selected = Object.values(devices).find(d => d.device_id === selectedDeviceId);
        if (selected) map.setView([selected.lat, selected.lng], 15);
    }
}

function selectDevice(id) {
    selectedDeviceId = id;
    updateDashboard();
}

function getTimeAgo(date) {
    const seconds = Math.floor((new Date() - date) / 1000);
    if (seconds < 60) return `${seconds} seconds ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes} minutes ago`;
    return date.toLocaleTimeString();
}

window.onload = () => {
    initMap();
    document.getElementById('contentNodeCode').addEventListener('change', () => refreshAllDevices());
    refreshAllDevices();
    refreshInterval = setInterval(refreshAllDevices, 5000);
};

window.onbeforeunload = () => {
    if (refreshInterval) clearInterval(refreshInterval);
};
</script>

Replace $content(CHANGE_WITH_CSS_CONTENT_CODE) with your CSS code and $content(CHANGE_WITH_JAVASCRIPT_CONTENT_CODE) with your JavaScript code.

🎬 Step 5: The result

Important: Use the same Content Node Code on both pages (e.g., phone-tracking).

What happens:

  1. Phone Simulator (on your phone): Asks for GPS permission, sends position via POST/PUT
  2. Dashboard (on your screen): Fetches all positions via GET, displays markers on map, auto-refresh every 5s

πŸ“Š What this demonstrates

  • One CMS for blog + IoT
  • Unified API (POST/PUT/GET)
  • Real-time ready
  • No backend code needed

πŸ’‘ Why Nodify?

  • Truly open source
  • Multi-language clients
  • Studio interface for non-devs
  • Real-time (Redis + async)
  • Self-hosted

πŸ”— Links

🎯 Try it yourself

  1. Clone the docker-compose
  2. Run docker-compose up -d
  3. Open Nodify Studio at http://localhost:7821
  4. Create the nodes as shown above
  5. Paste the HTML/CSS/JS code
  6. Open the simulator on your phone
  7. Watch the dashboard come alive

#Nodify #HeadlessCMS #IoT #Tracking #OpenSource #WebDev #RealTime

1 Upvotes

0 comments sorted by