Realtime features usually mean one of two things: paying for Firebase, or wrestling with WebSocket servers at 2 AM. Today we're doing neither.
We're going to build a live chat app where messages appear instantly across all connected browsers — using PocketBase Cloud for the backend and React for the frontend. No servers to configure, no WebSocket code to write. PocketBase handles realtime over Server-Sent Events (SSE), and the JS SDK wraps it in one function call.
Total time: about 5 minutes. Let's go. ⏱️
What we're building
- A public chat room (no auth, to keep this short)
- Messages stored in PocketBase (SQLite under the hood)
- Realtime updates via SSE — open two tabs and watch them sync
Step 1 — Deploy PocketBase (30 seconds, literally)
- Go to pocketbasecloud.com and sign up (Free plan works fine for this tutorial)
- Click Deploy, pick a region close to you (there are 6 — Germany, Finland, US East/West, Singapore...)
- Wait ~30 seconds. You'll get a URL like:
https://your-app.pocketbasecloud.com
That's a full PocketBase instance with SSL already configured. Open /_/ on that URL to access the admin dashboard and create your admin account:
https://your-app.pocketbasecloud.com/_/
Self-hosting fans: everything below works identically on a self-hosted instance. The cloud just skips the VPS + nginx + certbot ritual.
Step 2 — Create the messages collection
In the admin dashboard:
-
Collections → New collection → name it
messages(type: Base) - Add two fields:
-
username— Plain text, required -
text— Plain text, required
-
- Open the API Rules tab and set both List/Search rule and View rule and Create rule to empty (unlock them) so anyone can read and post.
⚠️ Empty rules = public access. Fine for a demo chat; for a real app you'd lock Create to authenticated users with a rule like @request.auth.id != "".
That's the entire backend. No migrations, no ORM, no REST controllers.
Step 3 — Scaffold the React app
npm create vite@latest pb-chat -- --template react
cd pb-chat
npm install pocketbase
npm run dev
Step 4 — The realtime magic
Replace src/App.jsx with this (~60 lines, that's the whole app):
import { useEffect, useRef, useState } from "react";
import PocketBase from "pocketbase";
// 👇 your PocketBase Cloud URL
const pb = new PocketBase("https://your-app.pocketbasecloud.com");
export default function App() {
const [messages, setMessages] = useState([]);
const [text, setText] = useState("");
const [username] = useState(
() => "guest-" + Math.random().toString(36).slice(2, 7)
);
const bottomRef = useRef(null);
useEffect(() => {
// 1. Load the latest 50 messages
pb.collection("messages")
.getList(1, 50, { sort: "created" })
.then((res) => setMessages(res.items));
// 2. Subscribe to realtime changes (SSE under the hood)
pb.collection("messages").subscribe("*", (e) => {
if (e.action === "create") {
setMessages((prev) => [...prev, e.record]);
}
});
// 3. Clean up the subscription on unmount
return () => pb.collection("messages").unsubscribe("*");
}, []);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
async function send(e) {
e.preventDefault();
if (!text.trim()) return;
await pb.collection("messages").create({ username, text });
setText(""); // no manual state update needed — SSE delivers it back
}
return (
<div style={{ maxWidth: 480, margin: "40px auto", fontFamily: "sans-serif" }}>
<h2>⚡ PB Cloud Chat — you are {username}</h2>
<div style={{ height: 360, overflowY: "auto", border: "1px solid #ddd", padding: 12, borderRadius: 8 }}>
{messages.map((m) => (
<p key={m.id}>
<b>{m.username}:</b> {m.text}
</p>
))}
<div ref={bottomRef} />
</div>
<form onSubmit={send} style={{ display: "flex", gap: 8, marginTop: 12 }}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type a message…"
style={{ flex: 1, padding: 8 }}
/>
<button>Send</button>
</form>
</div>
);
}
Run it, open two browser tabs, and send a message. Both tabs update instantly. 🎉
Notice the neat trick in send(): we don't append the message to local state ourselves. We just create() the record, and the realtime subscription delivers it back to every client — including the sender. One source of truth, zero sync bugs.
What's actually happening under the hood?
PocketBase's realtime API uses Server-Sent Events, not WebSockets:
- The SDK opens a long-lived HTTP connection to
/api/realtime - It submits which collections/records you want to watch
- The server pushes JSON events (
create/update/delete) down that stream
SSE is simpler than WebSockets (plain HTTP, auto-reconnect built into the browser) and passes through proxies and firewalls more reliably. For server→client pushes like chat, feeds, and dashboards, it's honestly all you need.
Step 5 — Ship it
Build the frontend:
npm run build
PocketBase Cloud includes frontend hosting on every plan (even Free), so you can deploy the dist/ folder right next to your backend — one dashboard, one URL, no CORS headaches. Or push it to any static host you like; the backend URL is public either way.
Where to go from here
- 🔐 Add auth: PocketBase ships with email/password + OAuth2 (Google, GitHub...) built in — then lock the Create rule to
@request.auth.id != "" - 🗑️ Handle
update/deleteevents in the subscription for message editing - 📄 Paginate history with
getList(page, perPage) - 🚀 Running multiple side projects? The Pro plan (from $13/mo) lets you run unlimited PocketBase instances on one dedicated server — many users fit 10–20 apps on it
Try it here 👉 pocketbasecloud.com — the Free plan is genuinely 30 seconds to a live backend.
If you build something with this, drop a link in the comments. I'd love to see it! 💬
Top comments (0)