Live updates in about 40 lines of JavaScript - plus the security rules most tutorials skip.
Real-time features usually mean running a WebSocket server. With Firestore, the browser subscribes to a query and Google's servers push every change to every connected client. This guide builds a public chat room with plain JavaScript and Vite, so nothing gets between you and the Firebase APIs - then locks it down properly.
firebaseConfig object - copy it.npm create vite@latest firestore-chat -- --template vanilla
cd firestore-chat
npm install
npm install firebase
Replace index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Firestore chat</title>
</head>
<body>
<h1>Chat</h1>
<ul id="messages"></ul>
<form id="form">
<input id="text" maxlength="500" autocomplete="off" placeholder="Say something" required />
<button>Send</button>
</form>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Replace src/main.js, pasting your own config at the top:
import { initializeApp } from "firebase/app";
import {
getFirestore,
collection,
addDoc,
query,
orderBy,
limit,
onSnapshot,
serverTimestamp,
} from "firebase/firestore";
const firebaseConfig = {
// paste the object from the Firebase console here
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const messages = collection(db, "messages");
const list = document.querySelector("#messages");
const form = document.querySelector("#form");
const input = document.querySelector("#text");
// Newest 50 messages, re-delivered whenever anything changes
const recent = query(messages, orderBy("createdAt", "desc"), limit(50));
onSnapshot(recent, (snapshot) => {
list.innerHTML = "";
[...snapshot.docs].reverse().forEach((doc) => {
const li = document.createElement("li");
li.textContent = doc.data().text; // textContent, never innerHTML, for user text
list.appendChild(li);
});
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = "";
await addDoc(messages, { text, createdAt: serverTimestamp() });
});
Three details matter here. The query is limited - without limit(), every visitor would download every message ever sent, and you pay per document read. Timestamps come from serverTimestamp(), so a visitor with a wrong clock can't reorder the chat. And messages are inserted with textContent, so nobody can inject HTML or scripts into other people's screens.
Right now production mode denies everything, so the chat won't work yet. In the console, open Firestore → Rules and replace them with:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /messages/{messageId} {
allow read: if true;
allow create: if request.resource.data.keys().hasOnly(['text', 'createdAt'])
&& request.resource.data.text is string
&& request.resource.data.text.size() > 0
&& request.resource.data.text.size() <= 500
&& request.resource.data.createdAt == request.time;
allow update, delete: if false;
}
}
}
These rules let anyone read messages and post new ones, but only with exactly two fields, a non-empty string of at most 500 characters, and a genuine server timestamp. Nobody can edit or delete messages, and every other collection stays locked. Click Publish.
npm run dev
Open the page in two browser windows side by side and send a message from one - it appears in both almost instantly. That's the whole real-time system: no server code, no WebSockets, no polling.
Build the site with npm run build, then either:
npm install -g firebase-tools), run firebase login and firebase init hosting (choose dist as the public directory), then firebase deploy.dist. The real-time part runs on Google's servers, so your host only serves files.On the free Spark plan, Firestore includes a daily allowance of document reads, writes and deletes (at the time of writing, 50,000 reads and 20,000 writes per day). Each message delivered to each open listener counts as a read, so a busy room with many viewers uses reads quickly. Keep queries limited, and if you upgrade to the pay-as-you-go Blaze plan, set a budget alert.
request.auth.uid on each message, and require it in the rules.request.auth.uid.