⚡ Front-end Real‑time SMS Listener: Push Verification Codes to the Browser with WebSocket
A passionate live‑coding guide by a front‑end engineer who hates polling. We'll make SMS actively kick your browser with fresh codes.
The Pain Point: Polling Is Like Checking Your Mailbox Every Second
You've built a Python script that reads SMS from a SIM800L module via serial. Now you want a web dashboard to show incoming verification codes. The old way? Set a setInterval to hit /sms/latest every 2 seconds. It works — but it's wasteful, slow, and makes your backend wake up constantly for empty responses. Imagine waiting for a package and walking to the front door every 10 seconds even when the postman hasn't arrived.
What if the postman could ring your doorbell instead? That's WebSocket. The server pushes an event the moment an SMS arrives, and the browser reacts instantly. No wasted requests, sub‑second latency, and a delightful user experience.
Architecture Evolution: From Pull to Push
With WebSocket, the data path becomes a real‑time pipeline. The moment our Python serial listener pushes a message into a thread‑safe queue, the Socket.IO handler grabs it and emits it to all connected clients. That's what we're going to build today.
🛠️ The Full Tech Stack
- Backend: Flask + Flask‑SocketIO (with eventlet for production async)
- Hardware side: Our existing
serial_reader.pythat puts SMS dicts into a globalmessage_queue(we'll reuse that) - Frontend: A single HTML page with Socket.IO client, modern CSS, and a neat notification card
🔥 Backend: The Real‑time Server
Create a new file websocket_server.py. It wraps our serial listener and exposes Socket.IO events.
from flask import Flask, render_template_string
from flask_socketio import SocketIO, emit
from serial_reader import message_queue, start_reader
import threading
import time
app = Flask(__name__)
app.config['SECRET_KEY'] = 'super-secret-sms-key'
socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading')
# Background task that polls the queue and broadcasts new SMS
def background_sms_broadcast():
while True:
while not message_queue.empty():
msg = message_queue.get_nowait()
# Extract verification code with regex
import re
code_match = re.search(r'\b(\d{4,6})\b', msg.get('text', ''))
msg['code'] = code_match.group(1) if code_match else None
# Emit to ALL connected clients (or use rooms for isolation)
socketio.emit('new_sms', msg)
socketio.sleep(0.1) # non‑blocking wait
@socketio.on('connect')
def handle_connect():
print('⚡ Client connected')
emit('status', {'msg': 'Connected to SMS relay'})
@socketio.on('disconnect')
def handle_disconnect():
print('🔌 Client disconnected')
@app.route('/')
def index():
# We'll serve the HTML file inline
return HTML_CONTENT # defined below
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--port', default='/dev/ttyUSB0')
args = parser.parse_args()
# Start serial reader in background thread
start_reader(args.port)
# Start the background broadcaster
socketio.start_background_task(background_sms_broadcast)
# Run the server
socketio.run(app, host='0.0.0.0', port=5000, debug=False)
Notice the cors_allowed_origins='*' – without it, if your frontend is served from a different port or domain, the browser will block the WebSocket handshake. We also used async_mode='threading' for simplicity; for production with many concurrent clients, swap to eventlet or gevent.
🎨 Frontend: The Live Dashboard
Now for the juicy part — a complete HTML page that connects to our Socket.IO server and displays SMS in real‑time with a slick animation.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SMS Live Monitor</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #e2e8f0;
display: flex; flex-direction: column; align-items: center; padding-top: 3rem; }
.card { background: #1e293b; border-radius: 14px; padding: 18px 24px; width: 360px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4); margin-bottom: 16px;
transform: translateY(20px); opacity: 0; transition: all 0.35s ease; }
.card.show { transform: translateY(0); opacity: 1; }
.sender { font-weight: 600; color: #818cf8; }
.code { font-size: 2.2rem; font-weight: 700; color: #facc15; letter-spacing: 3px; }
.text { color: #cbd5e1; margin-top: 6px; }
.time { font-size: 0.8rem; color: #64748b; }
.copied { color: #4ade80; margin-left: 8px; font-size: 0.8rem; display: none; }
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: #4ade80; margin-right: 6px; }
#status { margin-bottom: 20px; }
</style>
</head>
<body>
<div id="status"><span class="status-dot"></span> Connected</div>
<div id="message-container"></div>
<script>
const socket = io('http://localhost:5000', { transports: ['websocket', 'polling'] });
const container = document.getElementById('message-container');
socket.on('connect', () => {
document.getElementById('status').innerHTML = '<span class="status-dot"></span> Live';
});
socket.on('disconnect', () => {
document.getElementById('status').innerHTML = '<span style="color:red">●</span> Disconnected';
});
socket.on('new_sms', async (msg) => {
const card = document.createElement('div');
card.className = 'card';
const codeHTML = msg.code ? `<div class="code">${msg.code}</div>` : '';
card.innerHTML = `
<div class="sender">${msg.sender || 'Unknown'}</div>
${codeHTML}
<div class="text">${msg.text}</div>
<div class="time">${new Date(msg.time*1000).toLocaleTimeString()}</div>
<span class="copied" id="copy-${msg.time}">Copied!</span>
`;
container.prepend(card);
// Trigger animation
requestAnimationFrame(() => card.classList.add('show'));
// Auto‑copy code if present
if (msg.code) {
await navigator.clipboard.writeText(msg.code);
const copyEl = document.getElementById(`copy-${msg.time}`);
if (copyEl) { copyEl.style.display = 'inline'; setTimeout(() => copyEl.style.display = 'none', 1500); }
}
});
// Reconnection logic is built‑in, but we can tune it
socket.io.on('reconnect_attempt', (attempt) => {
console.log(`Reconnection attempt ${attempt}`);
});
</script>
</body>
</html>
This page connects to our backend using the Socket.IO client. When a 'new_sms' event fires, it creates a stylish card with the sender, extracted code, full text, and timestamp. The verification code is automatically copied to the clipboard — a tiny UX delight!
🎬 The Full Demo: See It in Action
Here’s how you test the full stack:
- Ensure your SIM800L is powered and connected via USB‑TTL.
- Run the server:
python websocket_server.py --port /dev/ttyUSB0 - Open your browser to
http://localhost:5000. You should see the green “Connected” indicator. - Use another phone to send an SMS to the module’s number. In less than a second, the card slides in with the verification code highlighted and copied to your clipboard.
The magic is real. No page refresh, no polling. The instant the serial thread reads the message, the WebSocket pushes it across the wire.
💥 Three Debugging Nightmares & Their Cures
1. CORS & 403 Forbidden during Handshake
WebSocket connection failed: Error during WebSocket handshake: Unexpected response code: 403. You double‑check the URL and port — everything seems fine.
Root cause: Flask‑SocketIO, by default, restricts CORS. If the HTML file is loaded from a different origin (even localhost:3000 vs localhost:5000), the server rejects the upgrade.
Fix: Set cors_allowed_origins='*' in the SocketIO constructor. In production, lock it down to your actual domain.
2. Disconnections & Silent Reconnect Loops
localStorage.debug = '*'; in browser console.
Fix: On the server, handle disconnect gracefully with no stale session expectations. Use @socketio.on('disconnect') only for cleanup. Also, configure client reconnection delay: reconnectionDelayMax: 5000.
3. Room Isolation for Multiple Users
from flask import session; socketio.emit('new_sms', msg, room=request.sid). The background broadcaster must know which room to target. For a single‑user dashboard, however, shared broadcast is fine.
🔒 Transport Upgrade & Fallbacks
Socket.IO starts with HTTP long‑polling by default and then upgrades to WebSocket if possible. You can watch the handshake in the browser’s Network tab: a request with 101 Switching Protocols confirms the upgrade. If the browser or proxy blocks WebSocket, Socket.IO seamlessly falls back to long‑polling — but the user experience remains the same because our code listens to 'new_sms' events regardless of the transport.
To enforce WebSocket‑only for speed, set transports: ['websocket'] on the client, but keep a fallback for compatibility. Our example above tries WebSocket first, then polling.
🚀 Beyond the Basics: Where to Take This
- PWA + Notification API: Turn the dashboard into a Progressive Web App that can send system notifications even when the browser tab is closed (on mobile).
- Multi‑number dashboard: Use separate “rooms” per SIM module and let users switch between number feeds.
- Logging & retention: Store received messages in a lightweight database (SQLite) and let the WebSocket replay the last 10 messages on connect.
- Secure token authentication: Pass a JWT during connection and validate it in the
connectevent to prevent unauthorized access.
By marrying a $5 GSM module with WebSocket, you’ve built a real‑time SMS pipeline that feels like a modern notification system. No more blind polling, no more refreshing. Just clean, instant delivery that makes your inner engineer smile.