Tutorial: answer questions from your documents (RAG)
Build a help-center assistant: load articles into a knowledge base, search it from your app with citations, add an agent that answers from it, and let each user add private files that only they can search.
A knowledge base splits your documents into sections, embeds them, and keeps them in a table in your project's Postgres with row-level security (Knowledge bases (RAG)). In this tutorial you load a help center, search it from a web app, and put an agent on top that answers with numbered citations. You need a project and @sovaria/sdk (Quickstart).
#1. Create the knowledge base
Run this as a team member (a signed-in console session or a server key), for example in a small Node script. app_users means signed-in app users can search what your team adds.
import { createClient } from '@sovaria/sdk'
// Server-only: the service role key comes from your secret store, never from the app.
const admin = createClient(process.env.SOVARIA_URL!, process.env.SOVARIA_SERVICE_ROLE_KEY!)
await admin.knowledge.create('help_center', { access: 'app_users', description: 'Public help articles' })#2. Load your articles
// Web pages: fetched and split by heading.
for (const url of ['https://example.com/help/refunds', 'https://example.com/help/shipping']) {
await admin.knowledge.addUrl('help_center', url)
}
// Your own text or Markdown, with a stable id and metadata to filter on.
const res = await admin.knowledge.addText('help_center', '# Gift cards\nGift cards can\'t be returned or exchanged…', {
documentId: 'gift-cards',
metadata: { area: 'billing' },
})
console.log(res) // sections, tokens, new / unchanged, still embedding, estimated_cost_usd- Re-adding a document with the same
documentIdreplaces it, and only changed sections are embedded again. Run the loader from CI whenever your help center changes. - Sections are searchable by exact words at once and by meaning within seconds, once they are embedded.
#3. Search from your app
// In the app, as the signed-in user.
const { results } = await sovaria.knowledge.search('help_center', 'can I return a gift card?', {
k: 5,
mode: 'hybrid', // meaning + exact words
metadataFilter: { area: 'billing' }, // optional
}) as any
for (const r of results) {
console.log(r.citation, r.title, '›', r.heading)
console.log(r.content.slice(0, 200))
}Showing the passages directly, with links to the source, is often all a help widget needs, with no model call and no AI cost per search.
#4. An agent that answers from it
Create the agent
Open Agents › New agent. Instructions: "Answer questions about our store from the help center. Cite sources like [1]. If the help center doesn't cover it, say so and offer to contact support." Choose your model provider and key.
Give it the knowledge base
In Knowledge, pick
help_center. Under Data access, choose No direct SQL: this agent only needs the documents.Open it to your users
Who can use it: your app's signed-in users. Set Runs per user per day (for example 30) so one person can't spend your model budget.
let threadId: string | undefined
export async function ask(question: string, onText: (t: string) => void) {
const reply = await sovaria.agents.run(AGENT_ID, {
message: question,
threadId, // continue the same conversation
onEvent: (e: any) => { if (e.type === 'text_delta') onText(e.text) },
})
threadId = reply.threadId
return reply
}The agent searches as the user who asked, so it only quotes passages that user may read. When the help center has no answer, it says it doesn't know instead of guessing.
#5. Private files per user (optional)
For "chat with my files", create a knowledge base with user uploads. Each file an app user adds is private to them: the table's row rules stop anyone else, the agent included, from retrieving it.
// Once, as a team member:
await admin.knowledge.create('my_files', { access: 'user_uploads', description: 'Files users upload' })
// In the app, as the signed-in user (a File from <input type="file">):
await sovaria.knowledge.addFile('my_files', file)
const { results } = await sovaria.knowledge.search('my_files', 'what does my lease say about pets?') as any#6. Keep the cost down
- You pay to embed each section once, and again only when it changes. Stable
documentIds keep unchanged sections from being re-embedded. - Sections of 300 to 600 tokens and a small embedding model with 512 dimensions are cheap and accurate enough for most help centers.
- Search alone uses no model. Only agent runs do, and they count against your Sovariq credits or your own provider key.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | Firebase Genkit RAG |
| Supabase | Supabase RAG with pgvector |
| Sovaria | Tutorial: answer questions from your documents (RAG) |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.