React
Build a React contact list on AgentDB. React runs in the browser, so it never holds the AgentDB key. It calls your backend, and the backend calls AgentDB.
Not run in CI. The React component below is not run in the test suite. The backend it calls is the html-node-backend starter, which the smoke test runs (npm run docs:starters).
#Where the key lives
On the backend only, in its environment: AGENTDB_WORKSPACE and AGENTDB_TOKEN (an agent key with role editor).
Build tools put some environment variables into the browser bundle. Never give the key a name that does that.
# Wrong: Vite copies every VITE_ variable into the JavaScript it sends to browsers.
VITE_AGENTDB_TOKEN=adbk.your-key# Secure: the backend reads this. The React build never sees it.
AGENTDB_TOKEN=replace-with-your-agent-key#The backend route
Use any backend from this section. The smallest is the Node server of the plain HTML guide: it serves GET /api/contacts?q= and POST /api/contacts.
const ALLOWED = {
"GET /api/contacts": async (url) => {
const q = (url.searchParams.get("q") ?? "").trim().slice(0, 100);
const data = await agentdb("POST", "/query", {
object: "contact",
where: { all: q ? [{ field: "name", op: "contains", value: q }] : [] },
sort: [{ field: "name", order: "asc" }],
limit: 50,
});
return [200, { contacts: data.records.map((r) => ({ id: r.id, name: r.data.name, email: r.data.email ?? null })) }];
},
"POST /api/contacts": async (_url, body, req) => {
const name = String(body.name ?? "").trim().slice(0, 200);
const email = String(body.email ?? "").trim().slice(0, 254);
if (!name) return [400, { error: "A contact needs a name." }];
// One id per submit from the page: a retried request is not written twice.
const submit = String(req.headers["x-submit-id"] ?? randomUUID()).replace(/[^A-Za-z0-9-]/g, "").slice(0, 64);
const rec = await agentdb("POST", "/records", { object: "contact", data: { name, ...(email && { email }) } }, `html-node.${submit}`);
return [201, { contact: { id: rec.id, name: rec.data.name, email: rec.data.email ?? null } }];
},
};During development, let Vite forward /api to that server. The browser then calls the same origin, and you need no CORS settings.
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: { proxy: { "/api": "http://localhost:3000" } },
});In production, serve the built files and the backend from one origin. See Deployment.
#The frontend
import { useEffect, useState } from "react";
export function Contacts() {
const [q, setQ] = useState("");
const [state, setState] = useState({ status: "loading", contacts: [], error: null });
useEffect(() => {
const ac = new AbortController();
setState((s) => ({ ...s, status: "loading" }));
fetch(`/api/contacts?q=${encodeURIComponent(q)}`, { signal: ac.signal })
.then(async (res) => {
const body = await res.json();
if (!res.ok) throw new Error(body.error ?? "Could not load contacts.");
setState({ status: "ready", contacts: body.contacts, error: null });
})
.catch((e) => {
if (e.name !== "AbortError") setState({ status: "error", contacts: [], error: e.message });
});
return () => ac.abort(); // a newer search cancels the older request
}, [q]);
return (
<section>
<input type="search" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search by name" />
{state.status === "loading" && <p role="status">Loading…</p>}
{state.status === "error" && <p role="alert">Error: {state.error}</p>}
{state.status === "ready" && state.contacts.length === 0 && <p>No contacts found.</p>}
<ul>
{state.contacts.map((c) => (
<li key={c.id}>{c.name}</li>
))}
</ul>
</section>
);
}React escapes {c.name} as text. Do not use dangerouslySetInnerHTML with data from AgentDB.
Adding a contact posts JSON to the backend:
async function addContact(name, email) {
const res = await fetch("/api/contacts", {
method: "POST",
headers: { "content-type": "application/json", "x-submit-id": crypto.randomUUID() },
body: JSON.stringify({ name, email }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.error ?? "Could not save.");
return body.contact;
}#Loading, empty and error states
Keep one status value: loading, ready or error. Show "no contacts" only when the status is ready and the list is empty. Show the backend's error message; it never holds internal details.
#Live updates
For changes made elsewhere, add an /events route to your backend that follows the change feed, then listen with EventSource in a useEffect. The live dashboard starter shows the backend. See Live data.
useEffect(() => {
const source = new EventSource("/events");
source.addEventListener("contacts", (e) => setLive(JSON.parse(e.data)));
return () => source.close();
}, []);