A multi-tenant AI website chatbot platform built with Spring Boot and Spring AI.
The system allows different websites to register their site, ingest their website content, store the content as vector embeddings in Qdrant, and provide an AI chatbot that answers questions using the website's own knowledge.
The chatbot uses Google Gemini for language generation and embeddings, Qdrant for vector search, and MySQL for site registration, conversations, administrators, and persistent chat memory.
JavaScript-rendered websites are processed using Playwright, allowing the ingestion system to extract content from modern React, Vue, Angular, and other client-rendered websites.
The project demonstrates practical implementation of modern AI application concepts including RAG, vector databases, embeddings, LLM integration, persistent AI memory, tenant isolation, website ingestion, browser automation, Spring Security, database migrations, and embeddable JavaScript widgets.
- π€ Google Gemini-powered AI chatbot
- π§ Retrieval-Augmented Generation (RAG)
- π Semantic similarity search using Qdrant
- π§© Gemini text embeddings for website knowledge
- π Website content ingestion using Playwright
- βοΈ Support for JavaScript-rendered websites
- βοΈ Automatic document chunking before embedding
- π’ Multi-site / multi-tenant architecture
- π Site-specific public keys for chatbot identification
- π Origin-based website authorization
- πΎ Persistent conversation memory using MySQL
- π§ Spring AI
MessageChatMemoryAdvisor - π Spring AI
QuestionAnswerAdvisor - π― Site-specific Qdrant metadata filtering
- π€ MySQL-backed administrator authentication
- π Spring Security Basic Authentication for admin APIs
- π Role-based admin authorization
- ποΈ MySQL persistence with Spring Data JPA
- π Flyway database migrations
β οΈ Global API exception handling- π¬ Embeddable JavaScript chatbot widget
- π± Responsive chatbot UI
- π³ Docker / Docker Compose support
- Administrator registers a website.
- Application generates a unique
siteIdand public key. - Website information is stored in MySQL.
- Administrator starts website ingestion.
- Playwright opens the registered website in a headless Chromium browser.
- JavaScript-rendered content is extracted from the page.
- Extracted content is converted into Spring AI
Documentobjects. - Documents are split into smaller chunks.
- Gemini generates embeddings for each chunk.
- Embeddings are stored in Qdrant with the corresponding
siteId. - A visitor opens the website chatbot widget.
- The widget sends the public key, conversation ID, and user message to the backend.
- The backend validates the website origin and resolves the registered site.
- Spring AI retrieves conversation memory from MySQL.
- Spring AI retrieves relevant website knowledge from Qdrant.
- Gemini receives the user question, conversation context, and retrieved website knowledge.
- Gemini generates the final response.
- The response is returned to the JavaScript chatbot widget.
User Question
β
βΌ
ββββββββββββββββββββββ
β ChatController β
βββββββββββ¬βββββββββββ
β
Public Key + Origin
β
βΌ
βββββββββββββββββββββββββββ
β Origin Validation β
β + Site Resolution β
ββββββββββββββ¬βββββββββββββ
β
βββββββββββββ΄ββββββββββββ
β β
βΌ βΌ
Chat Memory Qdrant
MySQL Site Knowledge
β β
β Similarity Search
β β
βββββββββββββ¬ββββββββββββ
β
βΌ
QuestionAnswerAdvisor
β
βΌ
ChatClient
β
βΌ
Google Gemini
β
βΌ
Response
Registered Website URL
β
βΌ
Playwright
β
Chromium Browser
β
βΌ
Execute JavaScript
β
βΌ
Rendered Web Page
β
βΌ
Extract Text
β
βΌ
Spring Document
β
βΌ
TokenTextSplitter
β
βΌ
Text Chunks
β
βΌ
Gemini Embedding Model
β
βΌ
3072-Dimensional Vector
β
βΌ
Qdrant
β
Metadata: siteId
When a user asks a question:
User
β
β "What projects are listed?"
βΌ
ChatClient
β
βββ MessageChatMemoryAdvisor
β β
β βββ MySQL conversation history
β
βββ QuestionAnswerAdvisor
β
βββ Qdrant similarity search
β
βββ siteId filter
β
βΌ
Relevant Documents
β
βΌ
Gemini
β
βΌ
Answer
The chatbot does not rely only on Gemini's general knowledge. Website-specific questions are grounded using the indexed website content.
Each registered website receives:
siteId
publicKey
domain
Example:
siteId:
site-c940c842
publicKey:
pk_96a828c3aee54d6b832a7c0c873062dd
domain:
https://www.raguls4.vercel.app
Website knowledge is stored in Qdrant with:
siteId = site-c940c842
When retrieving information, the chatbot applies the corresponding site filter.
This prevents knowledge belonging to one website from being retrieved for another website.
The chatbot widget sends:
X-Chatbot-Public-Key: pk_...
Origin: https://example.comThe backend:
Public Key
β
Find Site
β
Compare request Origin
β
Registered Domain
β
Allowed?
Example:
Registered:
https://www.raguls4.vercel.app
Request:
https://www.raguls4.vercel.app
β
β
Allowed
A different website using the same public key is rejected.
Administrative endpoints are protected using Spring Security and database-backed users.
Admin
β
β Basic Authentication
βΌ
Spring Security
β
βΌ
DaoAuthenticationProvider
β
βΌ
CustomUserDetailsService
β
βΌ
MySQL admin_users
β
βΌ
PasswordEncoder
β
βΌ
ROLE_ADMIN
β
βΌ
Admin API
Public chatbot requests do not require administrator credentials.
The chatbot stores conversation history using Spring AI's JDBC chat memory repository.
User Message
β
βΌ
MessageChatMemoryAdvisor
β
βΌ
MySQL
β
βΌ
SPRING_AI_CHAT_MEMORY
Conversation identifiers are generated by the widget using UUIDs and remain site-specific on the client side.
Conversation ownership is additionally maintained through the application's conversations table.
POST /api/sites{
"name": "Ragul Portfolio",
"domain": "https://raguls4.vercel.app/"
}Example response:
{
"siteId": "site-64c1f1ff",
"publicKey": "pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Ragul Portfolio",
"domain": "https://raguls4.vercel.app/"
}POST /api/sites/{siteId}/ingestExample response:
{
"siteId": "site-64c1f1ff",
"pagesDiscovered": 1,
"pagesIndexed": 1,
"chunksCreated": 2,
"failedPages": 0,
"status": "COMPLETED"
}POST /api/chatHeaders:
X-Chatbot-Public-Key: pk_xxxxxxxxx
Content-Type: application/json
Request:
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "What do you know about this website?"
}Response:
{
"response": "This website is the personal portfolio of Ragul S..."
}| Method | Endpoint | Description |
|---|---|---|
| POST | /api/chat |
Chat with the website-specific AI assistant |
| GET | /widget.js |
Serve embeddable chatbot widget |
All site-management APIs require ROLE_ADMIN.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/sites |
Register a website |
| POST | /api/sites/{siteId}/ingest |
Ingest website content |
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "What projects are available?"
}Header:
X-Chatbot-Public-Key: pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{
"response": "The website contains several projects including a URL Shortener, Chatter, Digital Banking System, and Secure GatePass Management System."
}The chatbot can be embedded into a website using a single script:
<script
src="https://your-chatbot-domain.com/widget.js"
data-site-key="pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
</script>The widget automatically:
- Loads the site's chatbot configuration
- Creates a site-specific conversation ID
- Displays the chatbot UI
- Sends messages to the Spring Boot backend
- Displays AI responses
- Preserves the conversation ID across page refreshes
| Category | Technology |
|---|---|
| Language | Java 21 |
| Backend Framework | Spring Boot 4.1.0 |
| AI Framework | Spring AI 2.0.0 |
| AI Model | Google Gemini |
| Embedding Model | Gemini Embedding |
| Vector Database | Qdrant |
| Database | MySQL 8 |
| ORM | Spring Data JPA / Hibernate |
| AI Memory | Spring AI JDBC Chat Memory |
| Website Rendering | Playwright |
| Security | Spring Security |
| Authentication | HTTP Basic Authentication |
| Password Hashing | BCrypt / Delegating Password Encoder |
| Database Migration | Flyway |
| Frontend Widget | Vanilla JavaScript |
| Containerization | Docker / Docker Compose |
| Utilities | Lombok |
src
βββ main
β βββ java
β β βββ com.ragul.ChatBot
β β βββ config
β β βββ controller
β β βββ dto
β β βββ entity
β β βββ exception
β β βββ repository
β β βββ security
β β βββ service
β β βββ util
β β
β βββ resources
β βββ application.yml
β βββ static
β β βββ widget.js
β βββ db
β βββ migration
β βββ V1__create_application_tables.sql
β βββ V2__create_admin_users.sql
β
β
βββ Dockerfile
βββ docker-compose.yml
βββ pom.xml
Flyway manages the application's database schema.
Stores registered websites and chatbot configuration.
id
site_id
public_key
name
domain
Associates conversations with their registered websites.
id
site_id
conversation_id
The pair:
site_id + conversation_id
is unique.
Stores administrative users.
id
username
password
role
enabled
Passwords are stored using a password encoder.
Spring AI's JDBC repository stores conversation messages.
conversation_id
content
type
timestamp
sequence_id
Qdrant stores embeddings generated from website content.
Collection:
website_knowledge
Each vector contains the associated website identifier as metadata:
siteId
This allows site-specific retrieval.
Example:
Question
β
Embedding
β
Qdrant similarity search
β
siteId filter
β
Relevant website chunks
Configure environment variables:
DB_PASSWORD=password
GEMINI_API_KEY=your-gemini-api-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-admin-passwordNever commit real API keys, passwords, or
.envfiles to the repository. Use.env.examplefor documentation.
server:
port: 8080
spring:
application:
name: ChatBot
datasource:
url: jdbc:mysql://localhost:3307/chatbot
username: root
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
flyway:
enabled: true
locations: classpath:db/migration
ai:
google:
genai:
api-key: ${GEMINI_API_KEY}
chat:
model: gemini-3.5-flash
temperature: 0.7
embedding:
api-key: ${GEMINI_API_KEY}
text:
model: gemini-embedding-001
vectorstore:
qdrant:
host: localhost
port: 6334
collection-name: website_knowledge
use-tls: false
initialize-schema: true
chat:
memory:
repository:
jdbc:
initialize-schema: always
app:
security:
development-origins:
- http://localhost:5173
- http://localhost:3000
admin:
username: ${ADMIN_USERNAME}
password: ${ADMIN_PASSWORD}git clone https://github.com/<your-username>/WebsiteAIChatbot.git
cd WebsiteAIChatbotCreate a .env file or configure the variables in your environment:
DB_PASSWORD=your-password
GEMINI_API_KEY=your-gemini-api-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-admin-passwordMake sure Docker is installed and running.
Start the required services:
docker compose up -dUsing Maven:
./mvnw spring-boot:runor:
mvn spring-boot:runBuild and start all services:
docker compose up --buildRun in detached mode:
docker compose up -dView running containers:
docker compose psView logs:
docker compose logs -fStop containers:
docker compose down| Service | Container | Port | Purpose |
|---|---|---|---|
| MySQL | mysql_chatbot |
3307 |
Application database |
| Qdrant | qdrant_chatbot |
6333 / 6334 |
Vector database |
| Backend | chatbot_backend |
8080 |
Spring Boot API and AI service |
βββββββββββββββββ
β Website β
β Visitor β
βββββββββ¬ββββββββ
β
widget.js
β
βΌ
ββββββββββββββββββββ
β Spring Boot β
ββββββββββ¬ββββββββββ
β
Origin + Public Key
β
βΌ
ββββββββββββββββββββββ
β Site Validation β
ββββββββββββ¬ββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
βΌ βΌ
MySQL Memory Qdrant
β β
β Site Filter
β β
ββββββββββββββ¬βββββββββββββ
βΌ
Spring AI
β
QuestionAnswerAdvisor
β
βΌ
ChatClient
β
βΌ
Google Gemini
β
βΌ
Response
The application uses a global exception handler to return consistent API errors.
Example:
{
"timestamp": "2026-08-18T16:07:12",
"status": 422,
"error": "INGESTION_FAILED",
"message": "Website ingestion failed.",
"path": "/api/sites/site-c940c842/ingest"
}Common responses include:
400 β VALIDATION_ERROR
401 β Unauthorized
403 β ORIGIN_NOT_ALLOWED
404 β SITE_NOT_FOUND
422 β INGESTION_FAILED
500 β INTERNAL_SERVER_ERROR
The application separates public chatbot access from administrative access.
publicKey
+
Origin
+
conversationId
The backend verifies that the website origin matches the registered site.
Username
+
Password
+
ROLE_ADMIN
are required for site registration and website ingestion.
This provides two separate security boundaries:
Website Visitor
β
Origin + Public Key
Administrator
β
Spring Security + ADMIN role
Run the Maven test suite:
./mvnw testor:
mvn testThe application can also be manually tested through Postman or the embedded chatbot widget.
The current ingestion pipeline processes the configured website URL as a rendered page using Playwright.
The current implementation does not yet provide:
- Automatic multi-page website crawling
- Incremental website re-indexing
- Background ingestion jobs
- PDF/document ingestion
- Advanced retrieval reranking
- Streaming AI responses
These can be added as future improvements.
- π·οΈ Multi-page website crawling
- π Incremental knowledge re-indexing
- π PDF and document ingestion
- β‘ Background ingestion jobs
- π JWT/OAuth2 admin authentication
- π₯ Multiple administrators with site-level permissions
- π¨ More widget customization
- π Streaming AI responses
- π Usage and token analytics
- π§ͺ Expanded unit and integration test coverage
- π Swagger/OpenAPI documentation
- π Monitoring with Prometheus and Grafana
- βοΈ Production deployment
- π³ Production container hardening
- βΈοΈ Kubernetes deployment
This project was built to explore practical implementation of:
Spring Boot
+
Spring AI
+
Large Language Models
+
Embeddings
+
Vector Databases
+
RAG
+
AI Memory
+
Multi-Tenant Architecture
+
Website Ingestion
+
Spring Security
The goal is to understand how modern AI capabilities can be integrated into a conventional backend application rather than building an isolated AI demo.
Contributions, suggestions, and improvements are welcome.
- Fork the repository
- Create a feature branch
- Commit your changes
- Push the branch
- Open a Pull Request
This project is licensed under the MIT License.