Svelte and SvelteKit
Build a contact list with SvelteKit. A server route (+server.ts) holds the AgentDB key. The page calls that route.
Not run in CI. The code below is not run in the test suite. It makes the same AgentDB calls as the starters that the smoke test runs.
#Where the key lives
In .env locally and in your host's environment in production. SvelteKit has two kinds of variables:
| Module | Who can read it | Use for the key? |
|---|---|---|
$env/static/private, $env/dynamic/private | Server code only | Yes |
$env/static/public (names start with PUBLIC_) | Also the browser | Never |
PUBLIC_AGENTDB_TOKEN=adbk.your-keyAGENTDB_WORKSPACE=ws_your_workspace_id
AGENTDB_TOKEN=replace-with-your-agent-keySvelteKit refuses to build when a page imports $env/static/private. That protects you from a key in the browser.
#The backend route
// src/routes/api/contacts/+server.ts
import { json, error } from "@sveltejs/kit";
import { AGENTDB_TOKEN, AGENTDB_WORKSPACE } from "$env/static/private";
const API = "https://app.agentdb.link";
async function agentdb(method: string, path: string, body?: unknown) {
const res = await fetch(`${API}/v1/workspaces/${AGENTDB_WORKSPACE}${path}`, {
method,
headers: { authorization: `Bearer ${AGENTDB_TOKEN}`, "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) error(res.status === 400 ? 400 : 502, data.error?.message ?? "The data service is not available.");
return data.data;
}
export async function GET({ 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 json({ contacts: data.records.map((r) => ({ id: r.id, name: r.data.name })) });
}
export async function POST({ request }) {
const body = await request.json();
const name = String(body.name ?? "").trim().slice(0, 200);
if (!name) error(400, "A contact needs a name.");
const rec = await agentdb("POST", "/records", { object: "contact", data: { name } });
return json({ contact: { id: rec.id, name: rec.data.name } }, { status: 201 });
}You can also load data in +page.server.ts. That file runs on the server too, so it may import the same helper.
#The frontend
<!-- src/routes/contacts/+page.svelte -->
<script>
let q = $state("");
let result = $state({ status: "loading", contacts: [], error: "" });
$effect(() => {
const ac = new AbortController();
result = { ...result, 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.message ?? body.error ?? "Could not load.");
result = { status: "ready", contacts: body.contacts, error: "" };
})
.catch((e) => {
if (e.name !== "AbortError") result = { status: "error", contacts: [], error: e.message };
});
return () => ac.abort();
});
</script>
<input type="search" bind:value={q} aria-label="Search by name" />
{#if result.status === "loading"}
<p role="status">Loading…</p>
{:else if result.status === "error"}
<p role="alert">Error: {result.error}</p>
{:else if result.contacts.length === 0}
<p>No contacts found.</p>
{/if}
<ul>
{#each result.contacts as c (c.id)}
<li>{c.name}</li>
{/each}
</ul>Svelte shows {c.name} as text. Do not use {@html} with data from AgentDB.
#Loading, empty and error states
One status field drives the page: loading, ready or error. SvelteKit's error() answers with { message }; the page shows it.
#Live updates
Poll the route every 10 to 30 seconds, or add a server-sent events route on a long-running Node server (adapter-node) that follows the change feed. The live dashboard starter shows that loop. See Live data.