Week 3 · Lesson 2 of 10

End-to-End Architecture for a Multi-User RAG Application

0% Complete

Overview

A practical RAG application can be understood as five connected layers: the browser interface, the back-end service, the data platform, the model layer, and the retrieval and processing layer. Each layer has a separate responsibility. Keeping these responsibilities visible makes the system easier to build, debug, secure, and deploy.

The browser interface is a React application written with TypeScript. React provides the interactive component model. TypeScript adds type safety so that message events, document records, configuration values, and API responses are less likely to be used incorrectly. Tailwind CSS provides utility-based styling, while a component library such as shadcn/ui supplies reusable controls including buttons, dialogs, forms, menus, and status elements. Vite acts as the front-end build tool. During development it runs a local server, compiles the source files, and provides hot reload so code changes appear quickly in the browser.

A representative local front-end address is localhost port 5173. The exact port can differ, but the important point is that the browser application runs as its own process.

The back end is a Python application using FastAPI. Python is a practical choice because AI SDKs, evaluation libraries, document parsers, embedding clients, and model integrations are commonly available there first. FastAPI provides HTTP endpoints and supports streamed responses. Uvicorn runs the application server, commonly on localhost port 8000 during development.

The front end communicates with the back end over HTTP for ordinary requests and server-sent events for streamed chat output. The stream may contain more than plain assistant text. It can include loading state, reasoning-tag content, tool-call events, subagent activity, errors, and final completion signals. This makes the streaming protocol part of the application architecture rather than a cosmetic feature.

The data platform is Supabase. It provides several functions in one system:

  • PostgreSQL for relational records.
  • pgvector for embedding storage and vector similarity search.
  • Object storage for original uploaded files.
  • Authentication for user sign-in.
  • Row-level security for user isolation.
  • Real-time updates so changes can appear in the browser without a manual refresh.

The back end writes documents, chunks, messages, settings, and other records to Supabase. The browser can also maintain a real-time connection so a document that moves from pending to processing to completed can update immediately. The same mechanism can remove a deleted document from the interface as soon as the database record is gone.

The model layer can use cloud services, local services, or both. Cloud model calls can go through providers that expose an OpenAI-compatible API. A routing service can make several cloud models available behind one interface. Local models can be served through LM Studio using the same general API format. This allows the back end to switch between local and cloud generation without replacing the application architecture.

The model layer should not be treated as one setting. At minimum, generation, embeddings, reranking, metadata extraction, and vision-language processing may use different models. The chat model that answers a user does not need to be the model that creates vectors. A reranker may be enabled only after retrieval. A local vision-language model may be used only when document parsing requires visual interpretation.

The document-processing layer uses Docling. Its standard pipeline can parse documents into structured text. It can also be paired with a vision-language pipeline when the source contains layouts, images, tables, or content that benefits from visual interpretation. Parsed text then flows into chunking, embedding, metadata extraction, and database storage.

The application has two primary user interfaces.

The chat interface contains persistent conversation threads, messages, streaming output, markdown rendering, memory, tool calls, subagent activity, loading state, and cancellation. A simple visual layout hides significant state-management complexity. The application must know which thread is active, store every user and assistant message, preserve tool events, reload the history after refresh, and keep the stream in chronological order.

The document interface manages ingestion. Users can drag and drop files, observe status, inspect extracted metadata, and delete records. The interface is only the visible surface of a larger pipeline that stores the file, parses it, chunks it, embeds it, enriches it, and writes all related records.

The local data flow for a document upload is:

  1. The browser submits a file to the FastAPI back end.
  2. The back end authenticates the user.
  3. The original file is written to a user-specific storage path.
  4. A document record is created with a pending or processing state.
  5. The parser extracts text.
  6. The record manager calculates a content hash and checks for duplication.
  7. Metadata is extracted and validated.
  8. Text is split into chunks.
  9. The embedding model converts each chunk into a vector.
  10. Document and chunk records are written to PostgreSQL and pgvector.
  11. The status becomes completed or failed.
  12. Real-time updates refresh the browser.

The local data flow for a question is:

  1. The browser sends the user message and thread identifier to the back end.
  2. The back end loads the permitted conversation history and active configuration.
  3. The main agent receives a system prompt and a set of available tools.
  4. The agent selects a retrieval strategy.
  5. Tool calls search documents, query structured data, search externally, or delegate document analysis.
  6. Results are placed into the model context.
  7. The model synthesizes an answer.
  8. Stream events are sent to the browser.
  9. User, assistant, and structured tool-event data are persisted.
  10. Observability traces record the model and tool interactions.

In a deployed architecture, the local codebase is committed to a remote Git repository. A push can trigger front-end and back-end builds. The front end may be deployed through a platform such as Vercel or Cloudflare, or through a server using Nginx. The back end runs FastAPI and Uvicorn on a separate service. A public front-end domain might be paired with an API subdomain. Authentication and the database remain central to both.

The same application can also run inside a private network. Supabase can be self-hosted, local models can replace cloud APIs, and the built front end and back end can be served behind a firewall. This creates a path to an air-gapped system. Air-gapped operation changes operational requirements because model files, parser dependencies, database services, and updates all need to be managed locally, but the logical architecture remains the same.

The implementation is easier to control when it is divided into eight modules:

Module 1 creates the application shell: authentication, threads, messages, streaming, persistence, and a basic managed-retrieval chat.

Module 2 adds the custom document interface and self-managed ingestion: file storage, chunking, embeddings, pgvector writes, provider abstraction, and real-time status.

Module 3 adds a record manager to prevent duplicate ingestion and manage changed content.

Module 4 adds structured metadata extraction, a configurable schema, metadata storage, and metadata-aware retrieval.

Module 5 adds multi-format parsing through Docling, including PDFs and office documents.

Module 6 replaces pure semantic retrieval with hybrid keyword and vector search, reciprocal rank fusion, and optional reranking.

Module 7 adds retrieval tools beyond the vector store, including web search and text-to-SQL against a restricted database surface.

Module 8 adds subagents for complete-document analysis while keeping the main agent context focused.

This sequence matters. It is tempting to start with the most visible agentic features, but those features depend on lower layers. A subagent cannot reliably analyze a document if ingestion is inconsistent. A reranker cannot improve search if the candidate set is wrong. A text-to-SQL tool is unsafe if database permissions are not enforced. An advanced chat interface is misleading if tool events disappear after refresh.

The architecture also exposes failure boundaries. A document can fail during upload, storage, parsing, metadata extraction, chunking, embedding, or database insertion. A chat can fail during authentication, history loading, model configuration, retrieval, tool execution, final synthesis, streaming, or persistence. Debugging is faster when these layers are explicit.

Back to top