Build a live order view with Express and Realtime
Run a complete local application: read a stored order, authorize its private channel, publish changes and recover after a disconnected socket. A second presence channel shows how multiple tabs share one customer identity.
This is an illustrative application. It assigns a local demo customer and advances an invented order; it does not authenticate real customers or fulfill purchases.
Prepare the app
Follow Create a Realtime app, then set BIRD_API_KEY, BIRD_REALTIME_APP_ID, BIRD_REALTIME_KEY, BIRD_REALTIME_SECRET and BIRD_REALTIME_REGION (us1 or eu1). The app ID, public key, secret and region must describe the same Realtime app. The API key needs the realtime scope.
Create an empty directory and install the packages:
Code example
npm init -y
npm install express @messagebird/sdk @messagebird/realtime
npm install --save-dev tsx esbuild typescript @types/express @types/node
mkdir publicThe browser receives only the public app key and region. The server keeps the API key and app secret.
Add the Express server
Save as server.ts. The file store belongs to this one-process example. The server writes an order and its pending-publication flag together, then retries publication independently. A later version can replace an earlier pending notification because clients fetch the current order rather than treating notifications as an event log.
Code example
import express from "express";
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { BirdClient } from "@messagebird/sdk";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Set ${name}.`);
return value;
}
const appKey = required("BIRD_REALTIME_KEY");
const appId = required("BIRD_REALTIME_APP_ID");
const region = required("BIRD_REALTIME_REGION");
if (region !== "us1" && region !== "eu1") throw new Error("Use us1 or eu1.");
const bird = new BirdClient({
apiKey: required("BIRD_API_KEY"),
realtime: { key: appKey, secret: required("BIRD_REALTIME_SECRET") },
});
const app = express();
app.use(express.json({ limit: "8kb" }));
const origin = "http://127.0.0.1:3000";
const sessions = new Map<string, { id: string; name: string }>();
const filename = "order-state.json";
type State = {
order: { id: string; customerId: string; status: string; version: number };
pending: boolean;
};
let state: State = existsSync(filename)
? JSON.parse(readFileSync(filename, "utf8"))
: {
order: { id: "42", customerId: "customer_ada", status: "placed", version: 1 },
pending: false,
};
function save(next: State) {
writeFileSync(filename + ".tmp", JSON.stringify(next));
renameSync(filename + ".tmp", filename);
state = next;
}
function userFor(req: express.Request) {
const token = (req.headers.cookie ?? "")
.split(";")
.map((x) => x.trim())
.find((x) => x.startsWith("demo_session="))
?.slice(13);
return token ? sessions.get(token) : undefined;
}
app.use((req, res, next) => {
res.setHeader("Cache-Control", "no-store");
if (req.method === "POST" && req.get("Origin") !== origin) {
res.sendStatus(403);
return;
}
next();
});
app.get("/", (req, res) => {
if (!userFor(req)) {
if (sessions.size >= 100) {
res.status(503).send("Restart this local demo to clear sessions.");
return;
}
const token = randomUUID();
sessions.set(token, { id: "customer_ada", name: "Ada" });
res.cookie("demo_session", token, { httpOnly: true, sameSite: "strict", maxAge: 3600000 });
}
res.type("html").send(readFileSync("public/index.html", "utf8"));
});
app.get("/client.js", (_req, res) =>
res.type("application/javascript").send(readFileSync("public/client.js", "utf8")),
);
app.get("/config", (_req, res) => res.json({ appKey, region }));
app.get("/orders/:id", (req, res) => {
const user = userFor(req);
if (!user || state.order.customerId !== user.id || req.params.id !== state.order.id) {
res.sendStatus(403);
return;
}
const { id, status, version } = state.order;
res.json({ id, status, version });
});
app.post("/bird/auth", async (req, res) => {
const user = userFor(req);
const { connection_id, channel_name } = req.body ?? {};
if (
!user ||
user.id !== state.order.customerId ||
!["private-order-42", "presence-order-42"].includes(channel_name)
) {
res.sendStatus(403);
return;
}
if (typeof connection_id !== "string" || !/^\d+\.\d+$/.test(connection_id)) {
res.sendStatus(400);
return;
}
const memberData =
channel_name === "presence-order-42"
? JSON.stringify({ member_id: user.id, member_info: { name: user.name } })
: undefined;
res.json(
await bird.realtime.authorizeChannel({
connectionId: connection_id,
channelName: channel_name,
memberData,
}),
);
});
app.post("/demo/advance", (req, res) => {
const user = userFor(req);
if (!user || user.id !== state.order.customerId) {
res.sendStatus(403);
return;
}
if (req.body?.expectedVersion !== state.order.version) {
res.status(409).json({ error: "Order changed; reload its current state." });
return;
}
const nextStatus: Record<string, string> = { placed: "packed", packed: "shipped" };
const status = nextStatus[state.order.status];
if (!status) {
res.status(409).json({ error: "The demo order is already shipped." });
return;
}
save({ order: { ...state.order, status, version: state.order.version + 1 }, pending: true });
res.json({ saved: true, version: state.order.version });
});
let publishing = false;
async function flush() {
if (!state.pending || publishing) return;
publishing = true;
const version = state.order.version;
try {
await bird.realtime.publish(appId, {
event: "order-updated",
channels: ["private-order-42"],
data: { id: "42", version },
});
if (state.order.version === version) save({ ...state, pending: false });
} catch (error) {
console.error("Order saved; publication will retry.", error);
} finally {
publishing = false;
}
}
setInterval(() => {
void flush();
}, 2000);
app.listen(3000, "127.0.0.1", () => console.log(origin));The expectedVersion check prevents a retry from advancing the order twice. A lost response followed by a retry can return 409; refresh the current order instead of blindly repeating the update with a newer version.
Add the browser client
Save as client.ts. The browser refreshes after subscription succeeds, including reconnection. It also keeps newer responses from being overwritten by earlier concurrent reads.
Code example
import { BirdRealtime } from "@messagebird/realtime";
const output = document.querySelector<HTMLPreElement>("#order")!;
const connection = document.querySelector<HTMLElement>("#connection")!;
const roster = document.querySelector<HTMLElement>("#roster")!;
const advance = document.querySelector<HTMLButtonElement>("#advance")!;
let version = 0;
let latestRequest = 0;
async function refresh() {
const requestNumber = ++latestRequest;
try {
const response = await fetch("/orders/42", { cache: "no-store" });
if (!response.ok) throw new Error("Open the demo again to renew its session.");
const order = await response.json();
if (requestNumber !== latestRequest || order.version < version) return;
version = order.version;
output.textContent = `Order ${order.id} · ${order.status} · version ${order.version}`;
advance.disabled = order.status === "shipped";
} catch (error) {
output.textContent = String(error);
}
}
await refresh();
const config = await (await fetch("/config")).json();
const bird = new BirdRealtime({ ...config, authEndpoint: "/bird/auth" });
const orders = bird.subscribe("private-order-42");
orders.bind("order-updated", () => {
void refresh();
});
orders.bind("bird:subscription_succeeded", () => {
void refresh();
});
orders.bind("bird:subscription_error", () => {
connection.textContent = "Order subscription refused. Refresh the session.";
});
bird.connection.bind("state_change", () => {
connection.textContent = `Connection: ${bird.connection.state}`;
if (bird.connection.state !== "connected")
roster.textContent = "Viewer list unavailable until reconnected";
});
const room = bird.subscribe("presence-order-42");
function renderMembers() {
roster.textContent = `Viewers: ${[...room.members.keys()].join(", ") || "none"}`;
}
room.bind("bird:subscription_succeeded", renderMembers);
room.bind("bird:member_added", renderMembers);
room.bind("bird:member_removed", renderMembers);
bird.connection.bind("unavailable", () => {
roster.textContent = "Viewer list reconnecting";
});
document.querySelector("#refresh")!.addEventListener("click", () => {
void refresh();
});
document.querySelector("#disconnect")!.addEventListener("click", () => bird.disconnect());
document.querySelector("#connect")!.addEventListener("click", () => bird.connect());
advance.addEventListener("click", async () => {
advance.disabled = true;
try {
const response = await fetch("/demo/advance", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ expectedVersion: version }),
});
if (!response.ok && response.status !== 409)
throw new Error("Update could not be confirmed; refresh the order.");
await refresh();
} catch (error) {
output.textContent = String(error);
advance.disabled = false;
}
});Save the page as public/index.html:
Code example
<!doctype html>
<html lang="en">
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Live order · Bird Realtime example</title>
<style>
body {
font: 16px/1.6 system-ui;
background: #f8f8f5;
color: #172f29;
max-width: 52rem;
margin: 8vh auto;
padding: 24px;
}
main {
border: 1px solid #d5dfd9;
border-radius: 20px;
padding: 32px;
background: white;
}
h1 {
font-size: 2.5rem;
line-height: 1.1;
}
pre {
white-space: pre-wrap;
background: #edf2ec;
padding: 24px;
border-radius: 12px;
}
button {
font: inherit;
border: 1px solid #c4d2c7;
border-radius: 24px;
padding: 8px 16px;
background: white;
margin: 4px;
}
button:focus-visible {
outline: 3px solid #47866d;
}
button:disabled {
opacity: 0.5;
}
</style>
<main>
<p>LOCAL APPLICATION EXAMPLE</p>
<h1>Your order, as it happens.</h1>
<p id="connection" role="status">Connecting</p>
<pre id="order" aria-live="polite">Loading order</pre>
<p id="roster">Loading viewers</p>
<button id="advance">Advance demo order</button><button id="refresh">Refresh order</button
><button id="disconnect">Disconnect socket</button
><button id="connect">Reconnect socket</button>
<p>
This example assigns the local demo customer Ada. Changes update only this demo order; no
fulfillment occurs.
</p>
</main>
<script type="module" src="/client.js"></script>
</html>Run and test
Bundle the client, then start the server from the same working directory:
Code example
npx esbuild client.ts --bundle --format=esm --outfile=public/client.js
npx tsx server.tsOpen http://127.0.0.1:3000 in two tabs. Both use the demo customer, so the presence list contains one member even though two connections are open. Advancing the order in one tab should update the other after publication and an authenticated refresh.
Select Disconnect socket in one tab, advance the order in the other, and reconnect. The disconnected tab must recover by fetching the current record after re-subscription; no event replay is assumed. Refresh order also works while the socket is disconnected.
A signed authorization response names an exact connection and channel. Try requesting private-order-99 or calling the authorization endpoint without its session cookie: the sample returns 403. Customer identity comes from the server's session, never member_id supplied by the browser.
To reset this invented order, stop the demo and remove only its order-state.json file. Restarting otherwise retains the order and any pending publication, while demo sessions reset.
Take the pattern into your application
Replace the demo session with your real authentication and authorize against your order owner. Use a transactional database and an outbox owned by your application when there are multiple server processes. Keep the order update and pending notification in one transaction, then publish after commit. Successful publication confirms acceptance by Realtime, not delivery to each browser.
The demo's Advance action is intentionally local. In a real shop, fulfillment services own order transitions; a customer's session grants read and subscription access, not permission to mark an order shipped. Use HTTPS and secure session cookies outside localhost. Limit session lifetime and subscription requests in the application.
Keep member information minimal: every presence subscriber can read it. A member is an identity, not a tab, a delivery acknowledgment or proof that the customer read the order.
Next steps
- Authorization contract
- Presence and member semantics
- Connection lifecycle
- Realtime troubleshooting
- Realtime resources and migrations
Related resources
Continue with the documentation, guides and examples for this topic. Resources are in English.