Picture this: you’re in a group chat planning a study session, but messages take ages to appear. Or imagine a food delivery app that doesn’t notify you when your order is at your door. These delays frustrate users. Real-time applications fix this by delivering instant updates, making apps feel responsive and engaging. Whether it’s a chat app like WhatsApp or push notifications on your phone, real-time tech is everywhere.
In this lecture, we’ll build a real-time chat application and explore mobile push notifications using modern full stack tools. We’ll use Next.js for the front end and implement three approaches: WebSockets with Socket.IO, Server-Sent Events (SSE) with Next.js API routes, and Push Notifications for mobile devices. By the end, you’ll understand how to create apps that update instantly and keep users connected.
A common misconception is that refreshing a page or polling (repeatedly checking for updates) is enough for real-time apps. Polling is slow, wastes server resources, and feels clunky. True real-time apps use WebSockets, SSE, or push notifications to deliver updates without constant requests. Another myth: real-time is only for chat apps. Nope! It’s used in live dashboards, stock tickers, collaborative tools like Google Docs, and mobile notifications for apps like Uber.
Our example will be a study group chat app where users can join a room, send messages, and receive instant updates. We’ll also add push notifications to alert users on their phones when new messages arrive.
Real-time apps enable instant data exchange between servers and clients. Think of it like a phone call (constant connection) versus texting (send and wait). Here are the technologies we’ll use:
Let’s debunk another misconception: some think real-time apps are too complex for beginners. Not true! With libraries like Socket.IO and Next.js’s API routes, you can build real-time features with minimal code. Our chat app will prove it.
WebSockets create a persistent, two-way connection, allowing instant communication. Socket.IO simplifies WebSocket setup and adds features like automatic reconnection.
We’ll build the chat app with an Express server and a Next.js front end, using TypeScript for type safety.
The server manages connections and broadcasts messages.
// server/index.ts
import express from 'express';
import http from 'http';
import { Server, Socket } from 'socket.io';
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: 'http://localhost:3000' },
});
interface Message {
user: string;
text: string;
}
interface SendMessagePayload {
room: string;
user: string;
text: string;
}
io.on('connection', (socket: Socket) => {
console.log('User connected:', socket.id);
socket.on('joinRoom', (room: string) => {
socket.join(room);
socket.broadcast.to(room).emit('message', {
user: 'System',
text: `User ${socket.id} joined!`,
});
});
socket.on('sendMessage', ({ room, user, text }: SendMessagePayload) => {
const message: Message = { user, text };
io.to(room).emit('message', message);
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
server.listen(4000, () => console.log('Server running on port 4000'));
The Next.js app connects to the server, joins a room, and displays messages.
// app/page.tsx
'use client';
import { useState, useEffect } from 'react';
import io, { Socket } from 'socket.io-client';
interface Message {
user: string;
text: string;
}
const socket: Socket = io('http://localhost:4000');
export default function Chat() {
const [room, setRoom] = useState<string>('study-group');
const [user, setUser] = useState<string>(
'Student' + Math.floor(Math.random() * 100),
);
const [text, setText] = useState<string>('');
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
socket.emit('joinRoom', room);
socket.on('message', (message: Message) => {
setMessages((prev) => [...prev, message]);
});
return () => {
socket.off('message');
};
}, [room]);
const sendMessage = () => {
if (text.trim()) {
socket.emit('sendMessage', { room, user, text });
setText('');
}
};
return (
<div className="p-4 max-w-md mx-auto">
<h1 className="text-2xl font-bold">Study Group Chat</h1>
<div className="border p-2 h-64 overflow-y-auto mb-2">
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.user}:</strong> {msg.text}
</div>
))}
</div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
className="border p-2 w-full"
placeholder="Type a message..."
/>
<button onClick={sendMessage} className="bg-blue-500 text-white p-2 mt-2">
Send
</button>
</div>
);
}
joinRoom
and sendMessage
events, with TypeScript interfaces ensuring correct data shapes.SSE is simpler than WebSockets and ideal for one-way updates (server to client). Next.js API routes let us implement SSE without a separate server.
We’ll modify the chat app to use SSE for receiving messages, with TypeScript for type safety.
Two API routes: one for streaming messages (SSE) and one for sending messages.
// app/api/messages/stream/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface Message {
user: string;
text: string;
}
let messages: Message[] = [];
let clients: TransformStreamDefaultController[] = [];
export async function GET() {
const stream = new ReadableStream({
start(controller) {
clients.push(controller);
controller.enqueue(`data: ${JSON.stringify(messages)}\n\n`);
const interval = setInterval(() => {
controller.enqueue(`data: ${JSON.stringify(messages)}\n\n`);
}, 1000);
return () => {
clearInterval(interval);
clients = clients.filter((c) => c !== controller);
};
},
});
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
export async function POST(request: NextRequest) {
const { user, text }: Message = await request.json();
messages.push({ user, text });
clients.forEach((client) =>
client.enqueue(`data: ${JSON.stringify(messages)}\n\n`),
);
return NextResponse.json({ success: true });
}
The client listens to the SSE stream and sends messages via POST.
// app/page.tsx
'use client';
import { useState, useEffect } from 'react';
interface Message {
user: string;
text: string;
}
export default function Chat() {
const [user, setUser] = useState<string>(
'Student' + Math.floor(Math.random() * 100),
);
const [text, setText] = useState<string>('');
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
const eventSource = new EventSource('/api/messages/stream');
eventSource.onmessage = (event) => {
const newMessages: Message[] = JSON.parse(event.data);
setMessages(newMessages);
};
return () => eventSource.close();
}, []);
const sendMessage = async () => {
if (text.trim()) {
await fetch('/api/messages/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user, text }),
});
setText('');
}
};
return (
<div className="p-4 max-w-md mx-auto">
<h1 className="text-2xl font-bold">Study Group Chat (SSE)</h1>
<div className="border p-2 h-64 overflow-y-auto mb-2">
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.user}:</strong> {msg.text}
</div>
))}
</div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
className="border p-2 w-full"
placeholder="Type a message..."
/>
<button onClick={sendMessage} className="bg-blue-500 text-white p-2 mt-2">
Send
</button>
</div>
);
}
/api/messages/stream
route streams messages to clients using SSE.EventSource
and displays new messages.Push notifications alert users on their phones, even when the app is closed. They’re perfect for urgent updates, like new chat messages when the user isn’t active.
We’ll add push notifications to our chat app, notifying users of new messages, using TypeScript.
Handles subscriptions and sends notifications using web-push
.
// app/api/notifications/route.ts
import { NextRequest, NextResponse } from 'next/server';
import webPush from 'web-push';
webPush.setVapidDetails(
'mailto:example@yourdomain.com',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!,
);
interface PushSubscription {
endpoint: string;
keys: {
p256dh: string;
auth: string;
};
}
interface NotificationPayload {
message: string;
}
let subscriptions: PushSubscription[] = [];
export async function POST(request: NextRequest) {
const subscription: PushSubscription = await request.json();
subscriptions.push(subscription);
return NextResponse.json({ success: true });
}
export async function PUT(request: NextRequest) {
const { message }: NotificationPayload = await request.json();
const payload = JSON.stringify({ title: 'New Message', body: message });
subscriptions.forEach((sub) => {
webPush.sendNotification(sub, payload).catch((err) => console.error(err));
});
return NextResponse.json({ success: true });
}
Registers for notifications and displays them.
// app/page.tsx (add to existing chat)
'use client';
import { useState, useEffect } from 'react';
interface Message {
user: string;
text: string;
}
export default function Chat() {
const [user, setUser] = useState<string>(
'Student' + Math.floor(Math.random() * 100),
);
const [text, setText] = useState<string>('');
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/sw.js').then((reg) => {
reg.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
})
.then((sub) => {
fetch('/api/notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sub),
});
});
});
}
}, []);
// ... rest of SSE chat code from Section 3 ...
}
// public/sw.ts
self.addEventListener('push', (event: PushEvent) => {
const data = event.data?.json();
if (data) {
self.registration.showNotification(data.title, { body: data.body });
}
});
When a message is sent, trigger a notification.
// app/api/messages/stream/route.ts (modified POST)
export async function POST(request: NextRequest) {
const { user, text }: Message = await request.json();
messages.push({ user, text });
clients.forEach((client) =>
client.enqueue(`data: ${JSON.stringify(messages)}\n\n`),
);
await fetch('/api/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: `${user}: ${text}` }),
});
return NextResponse.json({ success: true });
}
Approach | Use Case | Pros | Cons |
---|---|---|---|
WebSockets (Socket.IO) | Two-way apps (chat, gaming) | Fast, bidirectional, reliable | Resource-intensive, needs separate server |
SSE (Next.js) | One-way updates (feeds, notifications) | Simple, lightweight, no external server | One-way, less suited for complex interactions |
Push Notifications | Mobile/web alerts | Works offline, engaging | Complex setup, requires permissions |
For our chat app, WebSockets are best for real-time messaging, SSE is great for simpler updates, and push notifications keep users informed on the go.
By mastering these techniques, you can build apps that feel fast, responsive, and modern. Try combining WebSockets and push notifications in your chat app to create a seamless user experience!
Imagine planning a study session in a group chat, but messages lag. Frustrating, right? Real-time apps like WhatsApp solve this with instant updates. In this lecture, we’ll build a chat app using NextJS and explore WebSockets, SSE, and push notifications. Let’s make apps feel alive and responsive!
Instant updates enhance user experience. Real-time apps, like WhatsApp or live sports apps, keep users engaged by delivering updates instantly. Imagine a group chat where messages lag—frustrating! Real-time tech fixes this!
It's a common myth that polling is enough. Polling, or repeatedly checking for updates, is slow and wastes resources. Imagine 1000 users checking for new messages every second. What a waste of resources. True real-time uses WebSockets, SSE, or push notifications for instant data.
Our goal is to build a study group chat app. We’ll create a chat app where students join a room, send messages, and get instant updates, plus mobile notifications, using NextJS and TypeScript.
There are few technologies that power real-time applications.
WebSockets create two-way, persistent connections and allow servers and clients to send messages anytime, perfect for chat apps where users need instant back-and-forth.
Server-sent events, or SSE, push updates to the client. They are lightweight, ideal for one-way updates like live feeds or notifications, and work natively with NextJS.
Push notifications or mobile alerts reach users even when the app is closed, which is great for alerting users about new chat messages.
Webhooks let servers notify clients of events via HTTP, useful for triggering actions like sending a chat message to an external service.
Last, gRPC is a high-performance RPC framework. gRPC uses HTTP/2 for fast, typed communication, suited for backend services that need to push real-time chat data efficiently. In this lecture, we will focus on the first three technologies, web sockets, SSE and push notifications.
Each real-time technology has a different communication protocol and aims at different scenarios. Please study them on your own time or when you need to dig deeper.
Let's build backend for our chat application, using WebSockets.
WebSockets keep an open connection, allowing instant message exchange, which is ideal for our chat app’s real-time needs.
We will use SocketIO package for simplicity as SocketIO simplifies WebSocket setup with features like reconnection, making it beginner-friendly.
We’ll run a separate Express server alongside NextJS to handle WebSocket connections. Please not that you can not deploy this app to Vercel, as Vercel only supports server or lambda functions. If you want to deploy this, you will need a constantly running instance, such as EC2 on AWS.
This backend sets up a SocketIO server to handle real-time chat, allowing users to join rooms and exchange messages instantly.
We use the socketIO and express packages to create this server.
We launch a new server instance and set the CORS rules disabling requests from other apps, possibly spamming our server chat rooms.
Then, we define the types for messages exchanged between client and server assuring type safety.
This handles new user connections, creating a new socket and logging a message to the console.
We use the created socket to add message handlers, such as this one which handles the request for user to join a particular chat room, notifying all users in that room that the user has joined.
This handler notifies all users in the room that a new message has been sent by a particular users
And last, we have a handler when the user disconnects from the socket. The socketIO provides a natural interface to chat applications creation.
The client will be a standard NextJS app.
We use the client version of SocketIO library
We use the same message type as on the server.
Here, we initiate a connection to the socket server, running on port 3000.
We remember, which room we are currently in, setting the default to study group.
In the useEffect hook, we handle the WebSocket communication, joining the room from our state and listening to all incoming messages while updating the message state. Observe how we notify the socket about disconnection each time we execute this use effect.
Also, note, how we execute this connection initiation only when the desired room change.
The send message handler sends the message to the active socket.
All that remains is to render the UI and hook all handlers. Pretty simple, huh?
Let's explore Server Side Events or SSE and ways to implement them on server,
Server-Side Events are one-way, lightweight updates that let the server push messages to clients, perfect for simpler real-time updates like our chat’s message feed.
SSE uses NextJS API routes. There is no separate server needed, simplifying our stack.
SSE are best for server-to-client data. Unlike WebSockets, SSE is one-way, ideal for notifications or feeds but less suited for two-way chat.
Let's check out the server implementation of SSE.
We use the same message type as in the previous example.
We keep all connected clients in an array. Again, you will need a constantly running server, not a Vercel deployment.
We see that the implementation uses a native NextJS route accepting GET command.
When user visits this URL, the SSE create a readable stream where we push all the messages coming to the server,
Last, we need to return this readable stream setting the appropriate headers so that the client knows how to handle them.
The POST handler adds a new message to the list of messages, which are then streamed to the client. As you can see, we are always streaming all the messages. Not very efficient. Let's check out the client now!
This is the NextJS front end to our SSE. It listens to the SSE stream, using EventSource to receive messages from the server and update the chat UI in real-time. Unlike WebSockets, it sends messages using a standard HTTP POST request, keeping the setup simple.
On the initial component render, we create a new Event Source connecting to our API and listening to the incoming messages.
To send the chant message, we post the message to the designated chat route.
The UI closely resembles our previous example, displaying the list of messages along with the input field to type and send a message. This setup is considerably easier than WebSockets, although it has the limitation of a one-way communication protocol.
Let's wrap up real-time apps.
Use WebSockets for fast, bidirectional apps like our study group chat. You will need a real-time server hosted on your own server or EC2 AWS instance.
Use SSE for simple updates. NextJS API routes make SSE easy for one-way feeds or notifications.
Push notifications are great for engagement. They alert users offline with notifications while keeping them relevant. Although we did not show you an example of Push Notifications in this presentation, the lecture description illustrates how to implement it with service workers and the web push package.
And remember, polling is inefficient; always choose WebSockets, SSE, or notifications for real-time. That's it for now!