SvelteKit
Integrate Atlas into a SvelteKit app using @ai-sdk/svelte.
Integrate Atlas into a SvelteKit app using @ai-sdk/svelte.
Prerequisites: A running Atlas API server. See Bring Your Own Frontend for architecture and common setup.
AI SDK v6 — Svelte adapter
As of AI SDK v6, @ai-sdk/svelte (v4+) supports the transport-based API (DefaultChatTransport, sendMessage) shown below. Make sure you install @ai-sdk/svelte@^4.0.0 and ai@^6.0.0. If you prefer not to use streaming, the sync query endpoint (POST /api/v1/query) works with any HTTP client. The @useatlas/sdk package provides a typed client for that endpoint.
1. Install dependencies
bun add @ai-sdk/svelte ai2. Configure the API URL
Option A: Same-origin proxy (recommended)
In vite.config.ts, proxy /api to the Atlas API during development:
// vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
server: {
proxy: {
"/api": {
target: "http://localhost:3001",
changeOrigin: true,
},
},
},
});Option B: Cross-origin
# .env
PUBLIC_ATLAS_API_URL=http://localhost:3001# Atlas API .env
ATLAS_CORS_ORIGIN=http://localhost:51733. Chat store
Create a reusable chat module using @ai-sdk/svelte's useChat:
// src/lib/atlas-chat.ts
import { useChat } from "@ai-sdk/svelte";
import { DefaultChatTransport } from "ai";
import { writable, get } from "svelte/store";
import { env } from "$env/dynamic/public";
const apiUrl = env.PUBLIC_ATLAS_API_URL ?? "";
const isCrossOrigin = !!apiUrl;
export function createAtlasChat(apiKey: () => string) {
const conversationId = writable<string | null>(null);
const transport = new DefaultChatTransport({
api: `${apiUrl}/api/chat`,
get headers() {
const key = apiKey();
const h: Record<string, string> = {};
if (key) h["Authorization"] = `Bearer ${key}`;
return h;
},
credentials: isCrossOrigin ? "include" : undefined,
// Pass conversationId to continue an existing conversation
body: () => {
const convId = get(conversationId);
return convId ? { conversationId: convId } : {};
},
// Capture x-conversation-id from the response header
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await globalThis.fetch(input, init);
const convId = response.headers.get("x-conversation-id");
if (convId && convId !== get(conversationId)) {
conversationId.set(convId);
}
return response;
}) as typeof fetch,
});
const { messages, sendMessage, status, error } = useChat({ transport });
return { messages, sendMessage, status, error, conversationId };
}4. Chat page
Here is a minimal Svelte component that uses the chat store:
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { isToolUIPart } from "ai";
import { createAtlasChat } from "$lib/atlas-chat";
import ToolPart from "$lib/components/ToolPart.svelte";
let apiKey = $state("");
const { messages, sendMessage, status, conversationId } = createAtlasChat(
() => apiKey
);
let input = $state("");
const isLoading = $derived(
$status === "streaming" || $status === "submitted"
);
function handleSubmit() {
if (!input.trim()) return;
const text = input;
input = "";
sendMessage({ text });
}
</script>
<div class="mx-auto max-w-3xl p-4">
<h1 class="mb-4 text-xl font-bold">Atlas</h1>
<div class="space-y-4">
{#each $messages as m (m.id)}
{#if m.role === "user"}
<div class="flex justify-end">
<div class="rounded-lg bg-blue-600 px-4 py-2 text-white">
{#each m.parts ?? [] as part, i}
{#if part.type === "text"}
<p>{part.text}</p>
{/if}
{/each}
</div>
</div>
{:else}
<div class="space-y-2">
{#each m.parts ?? [] as part, i}
{#if part.type === "text" && part.text.trim()}
<div class="rounded-lg bg-zinc-100 px-4 py-2 text-sm">
{part.text}
</div>
{:else if isToolUIPart(part)}
<ToolPart {part} />
{/if}
{/each}
</div>
{/if}
{/each}
</div>
<form on:submit|preventDefault={handleSubmit} class="mt-4 flex gap-2">
<input
bind:value={input}
placeholder="Ask a question about your data..."
class="flex-1 rounded border px-4 py-2 text-sm"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
class="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-40"
>
Ask
</button>
</form>
</div>5. Rendering tool calls
In AI SDK v6, tool parts use per-tool type names ("tool-explore", "tool-executeSQL") or "dynamic-tool" for dynamic tools. Use isToolUIPart(part) from "ai" to detect tool parts and getToolName(part) to extract the name.
Here is a basic Svelte component for rendering tool calls:
<!-- src/lib/components/ToolPart.svelte -->
<script lang="ts">
import { getToolName } from "ai";
const { part } = $props<{ part: unknown }>();
let toolName = $derived.by(() => {
try {
return getToolName(part as Parameters<typeof getToolName>[0]);
} catch {
return "unknown";
}
});
const p = $derived(part as Record<string, unknown>);
const state = $derived((p.state as string) ?? "");
const input = $derived((p.input as Record<string, unknown>) ?? {});
const output = $derived(p.output);
const done = $derived(state === "output-available");
// SQL helpers
const sqlResult = $derived(output as Record<string, unknown> | null);
const columns = $derived((sqlResult?.columns as string[]) ?? []);
const rows = $derived((sqlResult?.rows as Record<string, unknown>[]) ?? []);
</script>
{#if !done}
<div class="my-2 animate-pulse rounded-lg border px-3 py-2 text-xs text-zinc-500">
{toolName === "executeSQL" ? "Executing query..." : "Running command..."}
</div>
{:else if toolName === "executeSQL" && sqlResult}
<div class="my-2 overflow-hidden rounded-lg border bg-zinc-50">
<div class="flex items-center gap-2 px-3 py-2 text-xs">
<span class="rounded bg-blue-100 px-1.5 py-0.5 font-medium text-blue-700">SQL</span>
<span class="flex-1 truncate text-zinc-500">{input.explanation ?? "Query result"}</span>
<span class="text-zinc-500">{rows.length} row{rows.length !== 1 ? "s" : ""}</span>
</div>
{#if columns.length && rows.length}
<div class="overflow-x-auto border-t">
<table class="w-full text-left text-xs">
<thead>
<tr class="border-b bg-zinc-100">
{#each columns as col}
<th class="px-3 py-1.5 font-medium">{col}</th>
{/each}
</tr>
</thead>
<tbody>
{#each rows as row, i}
<tr class="border-b">
{#each columns as col}
<td class="px-3 py-1.5 text-zinc-600">
{row[col] == null ? "\u2014" : row[col]}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{:else if toolName === "explore"}
<div class="my-2 rounded-lg border bg-zinc-50 px-3 py-2">
<div class="text-xs font-mono text-zinc-500">
<span class="text-green-500">$</span> {input.command}
</div>
<pre class="mt-1 max-h-40 overflow-auto whitespace-pre-wrap text-xs text-zinc-500">{output}</pre>
</div>
{:else}
<div class="my-2 rounded-lg border px-3 py-2 text-xs text-zinc-500">
Tool: {toolName}
</div>
{/if}The key data shapes:
executeSQLoutput --{ success, columns, rows, truncated }on success, or{ success: false, error }on failure. Userows.lengthto get the row count.exploreoutput -- a plain string (stdout of the command).state-- check for"output-available"to know when the result is ready. Other states:"input-streaming","input-available","output-error","output-denied".- Tool detection -- use
isToolUIPart(part)from"ai"(notpart.type === "tool-invocation").
See the Data Stream Protocol section for the full part structure and a concrete example.
6. Conversation management
Atlas persists conversations server-side (requires DATABASE_URL). Here is a minimal Svelte module for listing, loading, and deleting conversations:
// src/lib/atlas-conversations.ts
import { writable } from "svelte/store";
import { env } from "$env/dynamic/public";
const apiUrl = env.PUBLIC_ATLAS_API_URL ?? "";
export interface Conversation {
id: string;
userId: string | null;
title: string | null;
surface: string;
connectionId: string | null;
starred: boolean;
createdAt: string;
updatedAt: string;
}
export interface Message {
id: string;
conversationId: string;
role: "user" | "assistant" | "system" | "tool";
content: unknown;
createdAt: string;
}
export function createConversationStore(apiKey: () => string) {
const conversations = writable<Conversation[]>([]);
const loading = writable(false);
function headers(): Record<string, string> {
const key = apiKey();
const h: Record<string, string> = {};
if (key) h["Authorization"] = `Bearer ${key}`;
return h;
}
async function list() {
loading.set(true);
try {
const res = await fetch(`${apiUrl}/api/v1/conversations`, {
headers: headers(),
});
if (!res.ok) return;
const data = await res.json();
conversations.set(data.conversations ?? []);
} finally {
loading.set(false);
}
}
async function load(id: string): Promise<Message[]> {
const res = await fetch(`${apiUrl}/api/v1/conversations/${id}`, {
headers: headers(),
});
if (!res.ok) throw new Error("Failed to load conversation");
const data = await res.json();
return data.messages ?? [];
}
async function remove(id: string) {
await fetch(`${apiUrl}/api/v1/conversations/${id}`, {
method: "DELETE",
headers: headers(),
});
conversations.update((prev) => prev.filter((c) => c.id !== id));
}
return { conversations, loading, list, load, remove };
}To resume a conversation, pass the conversationId in the chat request body. The useChat composable from @ai-sdk/svelte does not manage this automatically, but the chat store in section 3 shows how to track the ID from the x-conversation-id response header and include it in subsequent requests via the transport's body option.
7. Synchronous queries (alternative)
If streaming is not needed, use the JSON query endpoint:
// src/lib/atlas-query.ts
import { env } from "$env/dynamic/public";
const apiUrl = env.PUBLIC_ATLAS_API_URL ?? "";
export async function queryAtlas(question: string, apiKey?: string) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${apiUrl}/api/v1/query`, {
method: "POST",
headers,
body: JSON.stringify({ question }),
});
if (!res.ok) throw new Error(`Atlas query failed: ${res.status}`);
return res.json();
}See Bring Your Own Frontend for the full architecture and what @atlas/web adds on top.