Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Intelligence View
⚡ tsecurity.de Intelligence

Real-Time Notifications in a MERN App — explained like texting your nerd bestie

You want that sweet little bell to light up the moment payments, bookings, or admin broadcasts happen — and you want the list to still be there after refresh. This guide takes you from nothing to a working MERN stack with: Socket.IO for …

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

You want that sweet little bell to light up the moment payments, bookings, or admin broadcasts happen — and you want the list to still be there after refresh. This guide takes you from nothing to a working MERN stack with:





  • Socket.IO for instant in‑app updates


  • MongoDB for persistence (history + unread)


  • REST endpoints to fetch + mark read


  • React hook + UI to display the dropdown



Let’s ship. 🚀









🧭 What we’re building



Two lanes make notifications feel “right”:





  1. Realtime lane → server emits Socket.IO events to the right users/roles.


  2. Persistence lane → server stores notifications in Mongo so they survive refresh/offline.



The client:




  • fetches last 50 notifications on mount,

  • listens for socket events and prepends new ones,

  • marks an item as read when clicked.




Result: real-time + durable notifications.










📦 Prereqs




  • Node 18+

  • npm or pnpm

  • A MongoDB connection (local or Atlas).



I’ll assume mongodb://localhost:27017/mern_notify locally.









🗂️ Project layout



We’ll create two folders:




mern-realtime-notify/
server/
client/












🛠️ Server (Express + Socket.IO + Mongo)






1) Init + install






mkdir -p mern-realtime-notify/server && cd mern-realtime-notify/server
npm init -y
npm i express cors dotenv mongoose socket.io jsonwebtoken bcrypt
npm i -D nodemon






Add scripts to package.json:




{
"name": "server",
"type": "module",
"scripts": {
"dev": "nodemon server.js"
}
}






Create .env:




PORT=5000
MONGO_URI=mongodb://localhost:27017/mern_notify
JWT_SECRET=dev_super_secret_change_me
CORS_ORIGINS=http://localhost:5173









2) Basic server with Socket.IO auth



server.js




import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import mongoose from 'mongoose'
import { createServer } from 'http'
import { Server } from 'socket.io'
import jwt from 'jsonwebtoken'

dotenv.config()

const app = express()
const server = createServer(app)

const allowed = (process.env.CORS_ORIGINS || '').split(',').filter(Boolean)
const io = new Server(server, {
cors: {
origin: allowed.length ? allowed : '*',
credentials: true,
},
transports: ['websocket', 'polling'],
})

app.use(cors({ origin: allowed.length ? allowed : '*', credentials: true }))
app.use(express.json())

// connect mongo
await mongoose.connect(process.env.MONGO_URI)
console.log('Mongo connected')

// ===== MODELS =====
import Notification from './src/models/Notification.js'
import User from './src/models/User.js'

// ===== ROUTES =====
import authRouter from './src/routes/auth.js'
import notifRouter from './src/routes/notifications.js'

// expose io to routes
app.use((req, _res, next) => { req.io = io; next() })

app.use('/api/auth', authRouter)
app.use('/api/notifications', notifRouter)

app.get('/api/health', (_req, res) => res.json({ ok: true }))

// ===== SOCKET AUTH =====
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth?.token
if (!token) return next(new Error('Missing token'))
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.userId).select('-password')
if (!user) return next(new Error('User not found'))
socket.user = user
next()
} catch (e) {
next(new Error('Auth failed: ' + e.message))
}
})

io.on('connection', (socket) => {
const user = socket.user
const userRoom = `user_${user._id}`
const roleRoom = `role_${user.role}`
socket.join(userRoom)
socket.join(roleRoom)
console.log(`Socket connected: ${user.email} -> rooms: ${userRoom}, ${roleRoom}`)

socket.on('disconnect', (reason) => {
console.log('Socket disconnected', reason)
})
})

const PORT = process.env.PORT || 5000
server.listen(PORT, () => console.log(`Server listening on ${PORT}`))









3) Models



src/models/User.js (super minimal; don’t use in prod without tweaks)




import mongoose from 'mongoose'
import bcrypt from 'bcrypt'

const userSchema = new mongoose.Schema({
email: { type: String, unique: true },
password: String,
role: { type: String, default: 'patient' } // 'patient' | 'doctor' | 'admin'
})

userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next()
this.password = await bcrypt.hash(this.password, 10)
next()
})

userSchema.methods.compare = function(pw) { return bcrypt.compare(pw, this.password) }

export default mongoose.model('User', userSchema)






src/models/Notification.js




import mongoose from 'mongoose'

const notificationSchema = new mongoose.Schema(
{
userId: { type: mongoose.Schema.Types.ObjectId, index: true },
type: { type: String, required: true }, // e.g., 'payment_success'
title: String,
message: String,
meta: Object,
readAt: Date,
},
{ timestamps: { createdAt: true, updatedAt: true } }
)

export default mongoose.model('Notification', notificationSchema)









4) Auth routes (login/register + JWT)



src/routes/auth.js




import express from 'express'
import jwt from 'jsonwebtoken'
import User from '../models/User.js'

const router = express.Router()

router.post('/register', async (req, res) => {
const { email, password, role = 'patient' } = req.body
const exists = await User.findOne({ email })
if (exists) return res.status(400).json({ success: false, message: 'Email in use' })
const user = await User.create({ email, password, role })
res.json({ success: true, user: { id: user._id, email: user.email, role: user.role } })
})

router.post('/login', async (req, res) => {
const { email, password } = req.body
const user = await User.findOne({ email })
if (!user || !(await user.compare(password))) {
return res.status(401).json({ success: false, message: 'Invalid credentials' })
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '7d' })
res.json({ success: true, token, user: { id: user._id, email: user.email, role: user.role } })
})

export default router









5) Notifications routes (fetch + markRead + test)



src/routes/notifications.js




import express from 'express'
import jwt from 'jsonwebtoken'
import Notification from '../models/Notification.js'

const router = express.Router()

// simple auth middleware reading Bearer token
function authenticate(req, res, next) {
try {
const h = req.headers.authorization || ''
const token = h.startsWith('Bearer ') ? h.slice(7) : null
if (!token) return res.status(401).json({ success: false, message: 'No token' })
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = { _id: decoded.userId }
next()
} catch (e) {
res.status(401).json({ success: false, message: 'Invalid token' })
}
}

// GET: latest 50
router.get('/', authenticate, async (req, res) => {
const list = await Notification.find({ userId: req.user._id }).sort({ createdAt: -1 }).limit(50).lean()
res.json({ success: true, notifications: list })
})

// POST: mark read
router.post('/read', authenticate, async (req, res) => {
const { id } = req.body
await Notification.updateOne({ _id: id, userId: req.user._id }, { $set: { readAt: new Date() } })
res.json({ success: true })
})

// POST: test -> store + emit
router.post('/test', authenticate, async (req, res) => {
const { message = 'This is a test', type = 'info', title = 'Test notification' } = req.body
const doc = await Notification.create({ userId: req.user._id, type, title, message })
req.io.to(`user_${req.user._id}`).emit('notification', {
_id: doc._id, type, title, message, createdAt: doc.createdAt, readAt: null
})
res.json({ success: true })
})

export default router






Start the server:




npm run dev












🎨 Client (React + Vite)






1) Init + install






cd ../
npm create vite@latest client -- --template react
cd client
npm i socket.io-client axios react-hot-toast






Add .env (Vite expects VITE_ prefix):




VITE_API_URL=http://localhost:5000/api
VITE_SOCKET_URL=http://localhost:5000









2) Minimal API helper



src/lib/api.js




import axios from 'axios'
const api = axios.create({ baseURL: import.meta.env.VITE_API_URL })
export function setAuth(token) {
api.defaults.headers.common['Authorization'] = token ? `Bearer ${token}` : undefined
}
export default api









3) Auth context (store token + user)



src/contexts/AuthContext.jsx




import { createContext, useContext, useState } from 'react'
import api, { setAuth } from '../lib/api'

const AuthCtx = createContext(null)
export const useAuth = () => useContext(AuthCtx)

export default function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [token, setToken] = useState(null)

const login = async (email, password) => {
const res = await api.post('/auth/login', { email, password })
if (res.data.success) {
setToken(res.data.token)
setAuth(res.data.token)
setUser(res.data.user)
}
return res.data
}

const register = async (email, password, role='patient') => {
const res = await api.post('/auth/register', { email, password, role })
return res.data
}

const logout = () => {
setToken(null); setUser(null); setAuth(null)
}

return (
<AuthCtx.Provider value={{ user, token, login, register, logout }}>
{children}
</AuthCtx.Provider>
)
}









4) Socket context (auth handshake)



src/contexts/SocketContext.jsx




import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { io } from 'socket.io-client'
import { useAuth } from './AuthContext'

const SocketCtx = createContext(null)
export const useSocket = () => useContext(SocketCtx)

export default function SocketProvider({ children }) {
const { token } = useAuth()
const [isConnected, setIsConnected] = useState(false)
const socketRef = useRef(null)

useEffect(() => {
if (!token) return
const socket = io(import.meta.env.VITE_SOCKET_URL, {
auth: { token },
transports: ['websocket', 'polling'],
})
socketRef.current = socket
socket.on('connect', () => setIsConnected(true))
socket.on('disconnect', () => setIsConnected(false))
return () => socket.disconnect()
}, [token])

return (
<SocketCtx.Provider value={{ socket: socketRef.current, isConnected }}>
{children}
</SocketCtx.Provider>
)
}









5) Notifications hook (fetch + live + markRead)



src/hooks/useNotifications.js




import { useEffect, useMemo, useState } from 'react'
import api from '../lib/api'
import { useSocket } from '../contexts/SocketContext'

const EVENTS = [
'notification', // generic catch-all we emit in /notifications/test
// add more if your server emits: 'payment_success', 'new_appointment', etc.
]

export function useNotifications() {
const { socket } = useSocket()
const [items, setItems] = useState([])

useEffect(() => {
let dead = false
;(async () => {
try {
const res = await api.get('/notifications')
if (!dead && res.data?.success) setItems(res.data.notifications)
} catch {}
})()
return () => { dead = true }
}, [])

useEffect(() => {
if (!socket) return
const on = (type) => (p) => setItems(prev => [normalize(type, p), ...prev])
EVENTS.forEach(evt => socket.on(evt, on(evt)))
return () => { EVENTS.forEach(evt => socket.off(evt)) }
}, [socket])

const unreadCount = useMemo(() => items.filter(n => !n.readAt).length, [items])

const markRead = async (id) => {
setItems(prev => prev.map(n => (n._id === id ? { ...n, readAt: new Date().toISOString() } : n)))
try { await api.post('/notifications/read', { id }) } catch {}
}

return { items, unreadCount, markRead }
}

function normalize(type, p) {
return {
_id: p._id || (Math.random().toString(36).slice(2) + Date.now().toString(36)),
type,
title: p.title || 'Notification',
message: p.message || '',
createdAt: p.createdAt || p.timestamp || new Date().toISOString(),
readAt: p.readAt || null,
...p,
}
}









6) Tiny UI (login + bell)



src/App.jsx




import { useState } from 'react'
import AuthProvider, { useAuth } from './contexts/AuthContext'
import SocketProvider from './contexts/SocketContext'
import { useNotifications } from './hooks/useNotifications'

function Login() {
const { login, register } = useAuth()
const [email, setEmail] = useState('[email protected]')
const [password, setPassword] = useState('secret')

return (
<div style={{ display:'flex', gap: 8, marginBottom: 16 }}>
<input value={email} onChange={e=>setEmail(e.target.value)} placeholder="email" />
<input value={password} onChange={e=>setPassword(e.target.value)} placeholder="password" type="password" />
<button onClick={() => register(email, password)}>Register</button>
<button onClick={() => login(email, password)}>Login</button>
</div>
)
}

function Bell() {
const { items, unreadCount, markRead } = useNotifications()
return (
<div>
<button>🔔 {unreadCount > 0 ? `(${unreadCount})` : ''}</button>
<div style={{ border:'1px solid #ddd', padding: 8, width: 360, marginTop: 8 }}>
{items.map(n => (
<div key={n._id} onClick={()=>markRead(n._id)} style={{ padding: 8, borderBottom:'1px solid #eee', cursor:'pointer' }}>
<div style={{ fontWeight:'bold' }}>{n.title}</div>
<div style={{ fontSize: 12, opacity:.7 }}>{new Date(n.createdAt).toLocaleString()}</div>
{n.message && <div style={{ fontSize: 13, marginTop: 4 }}>{n.message}</div>}
</div>
))}
</div>
</div>
)
}

export default function App() {
return (
<AuthProvider>
<SocketProvider>
<div style={{ padding: 24, fontFamily: 'sans-serif' }}>
<h1>MERN Real-time Notifications</h1>
<Login />
<Bell />
</div>
</SocketProvider>
</AuthProvider>
)
}






Run the client:




npm run dev






Open http://localhost:5173/. Register, then login.









🧪 Test it (Does it ding?)



With the client logged in, hit the test endpoint from a terminal:




curl -X POST http://localhost:5000/api/notifications/test \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_HERE" \
-d '{"message":"Hello from test!","type":"info"}'






You should see a new row appear under the bell instantly. Click it → it’s marked read. Refresh → it’s still there. ✅









🧯 Common gotchas





  • CORS: Allow your client origin on both Express and Socket.IO CORS.


  • Auth handshake: Send auth: { token } when connecting the socket (we did).


  • Cleanup: Always off() socket listeners on unmount (the hook does).


  • Reverse proxy: Enable WebSocket upgrades on Nginx/Cloudflare/etc.


  • Security: On mark-read, always filter by { _id, userId } (we do).









🧰 Extras (when you want more)





  • Role broadcasts: Server joins users to role_admin, role_doctor, role_patient. Emit to a role room.


  • Scheduled reminders: Add Redis + BullMQ → schedule “appointment in 1h” jobs, emit at the right time.


  • Web Push/FCM: For OS-level notifications when the tab is closed.


  • Hosted websockets: Pusher/Ably/PubNub if you don’t want to run Socket.IO infra.


  • Emails/SMS: SendGrid/SES and Twilio for critical backups.









🏁 Recap




  • Mongo stores notifications.

  • Socket.IO delivers them in real-time.

  • React hook keeps UI in sync and handles “mark read”.



This pattern is tiny, fast, and rock-solid. You can paste it into any MERN app and be “the person who made the bell work” by lunch. 🔔✨

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Real-Time Notifications in a MERN App — explained like texting your nerd bestie

Thematisch verwandte Begriffe: RealTime, Notifications, MERN, explained · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-96258 | A vulnerability has been found in onSite internet GmbH Auktion NG Auktio…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick