Technology Aug 15, 2026 · 5 min read

How I Built a Multi-Tenant RAG Knowledge Base with Source-Cited Answers — Pipeline, Multi-Tenancy, and Lessons

Every "build a RAG chatbot" tutorial ends the same way: embed a few paragraphs, call similaritySearch, print the answer. That gets you a demo, not a product. The gap between a RAG demo and a RAG product you'd trust with a company's documents is where all the real engineering lives. I built KnowBase...

DE
DEV Community
by Raja Abbas Affandi
How I Built a Multi-Tenant RAG Knowledge Base with Source-Cited Answers — Pipeline, Multi-Tenancy, and Lessons

Every "build a RAG chatbot" tutorial ends the same way: embed a few paragraphs, call similaritySearch, print the answer. That gets you a demo, not a product. The gap between a RAG demo and a RAG product you'd trust with a company's documents is where all the real engineering lives.

I built KnowBase AI, a multi-tenant SaaS knowledge base where businesses upload documents and an AI assistant answers questions grounded in their own content — with source citations you can click. This post covers the RAG pipeline, how multi-tenancy changes the design, and the decisions I'd repeat.

Live demo: knowbase-ai.netlify.app — no login needed, fully functional (it runs in demo mode with mock responses).

What RAG actually requires

RAG sounds simple: retrieve relevant context, feed it to the LLM, get a grounded answer. In production it means:

  • Ingestion: upload documents, chunk them intelligently, store them
  • Retrieval: find the chunks that actually answer the question
  • Grounding: build a prompt from the retrieved context, no hallucinated answers
  • Trust: show the user why the model said that — source citations

Each step is a small product on its own. Here's the pipeline.

The ingestion pipeline

Documents arrive as files, URLs, or manual entries. The key decision is chunking — too big and retrieval is fuzzy, too small and you lose context. The pipeline chunks text with overlap so no meaning falls through the gaps:

export function chunkText(text: string, size = 800, overlap = 200): string[] {
  const chunks: string[] = [];
  let i = 0;
  while (i < text.length) {
    chunks.push(text.slice(i, i + size));
    i += size - overlap;
  }
  return chunks;
}

Each chunk becomes a DocumentChunk tied to its source document, so retrieval can always trace back to where the information came from.

Multi-tenancy changes everything

A single-user RAG app and a multi-tenant SaaS share almost no code after the demo stage. Every query, chunk, and conversation must be scoped to a workspace:

// Every AI retrieval is scoped by workspaceId — a tenant can never
// retrieve another tenant's chunks, even if the embedding matches.
export async function retrieve(workspaceId: string, query: string) {
  return prisma.documentChunk.findMany({
    where: {
      document: { source: { workspaceId } },
      text: { contains: query },
    },
    take: 5,
  });
}

The data model enforces isolation at the schema level:

  • Workspace — tenant container
  • WorkspaceMember — roles: Owner / Admin / Member (RBAC via NextAuth.js v5)
  • KnowledgeSource + Document + DocumentChunk — the RAG layer, always under a workspace
  • Conversation + Message — chat sessions, scoped per workspace
  • ApiUsage — token tracking per workspace

The provider abstraction (OpenAI / Gemini / Claude)

One of the best decisions: never hard-code a model. A thin provider interface means the product runs on OpenAI, Google Gemini, or Anthropic Claude by configuration, and it made the demo mode trivial:

export interface AIProvider {
  chat(messages: Message[]): AsyncIterable<string>;
}

export const providers = {
  openai: OpenAIProvider,
  gemini: GeminiProvider,
  claude: ClaudeProvider,
};

// No API keys configured? Run a fully functional mock.
export function getProvider(): AIProvider {
  const configured = Object.entries(providers)
    .find(([, P]) => new P().isConfigured());
  return configured ? new configured[1]() : new MockProvider();
}

Demo mode was the reason I could publish a real demo without leaking keys or asking visitors to sign up.

Source citations — the feature that makes it trustworthy

An AI answer with no receipts is just a guess. KnowBase streams responses over SSE and attaches the chunks that informed each answer, so users can click through to the source document:

// Simplified — stream tokens, then emit the citations that grounded them
export async function chat(conversationId: string, content: string) {
  const chunks = await retrieve(workspaceId, content);
  const stream = await provider.chat([
    { role: "system", content: buildRagPrompt(chunks) },
    ...history,
    { role: "user", content },
  ]);

  return new Response(sse(stream, chunks), {
    headers: { "Content-Type": "text/event-stream" },
  });
}

This single feature separates a gimmick from a support tool. Customer-support teams don't trust "trust me" — they trust a cited answer they can verify in two clicks.

The stack

Layer Technology
Framework Next.js 16 (App Router, Turbopack)
Language TypeScript (strict)
Database SQLite + Prisma 7 (@prisma/adapter-libsql)
Auth NextAuth.js v5 (Auth.js) + JWT
AI OpenAI / Gemini / Claude (provider abstraction)
Styling Tailwind CSS v4 + shadcn/ui
Charts / Markdown Recharts + React Markdown (remark-gfm)
Forms React Hook Form + Zod
Deployment Netlify

I'm a full-stack developer at RA Technologies, where we build SaaS and AI products with exactly this architecture — one codebase, strict types, a database that needs no server, and a demo anyone can click.

Lessons learned

  1. SQLite-first was the right call. @prisma/adapter-libsql gives you a real production-ish setup with zero config. Swap to PostgreSQL later only if you actually need it — most knowledge-base products don't.
  2. Scoping is security. Multi-tenant bugs are data-leak bugs. Enforce workspaceId at the query layer, never rely on the UI to filter.
  3. A working demo beats a password gate. Removing auth for the MVP made the demo clickable for everyone — and it still demonstrates the full RAG experience.

Try it

If you've built RAG in production, what did I get wrong? I'd love to hear it in the comments.

DE
Source

This article was originally published by DEV Community and written by Raja Abbas Affandi.

Read original article on DEV Community
Back to Discover

Reading List