AgentDBDocs

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:

ModuleWho can read itUse for the key?
$env/static/private, $env/dynamic/privateServer code onlyYes
$env/static/public (names start with PUBLIC_)Also the browserNever
Shell: wrong, do not copy
PUBLIC_AGENTDB_TOKEN=adbk.your-key
Shell: secure, do this
AGENTDB_WORKSPACE=ws_your_workspace_id
AGENTDB_TOKEN=replace-with-your-agent-key

SvelteKit refuses to build when a page imports $env/static/private. That protects you from a key in the browser.

#The backend route

TypeScript
// 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

Svelte
<!-- 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.