Turn a website, PDFs or plain text into a grounded support chatbot you can embed with one script tag.
Botla is a multi-tenant RAG chatbot platform. It crawls and extracts your content, chunks and embeds it into Qdrant, and answers visitor questions with an LLM that is given only the retrieved context. When retrieval confidence is low, it falls back to a refusal or a human handoff instead of guessing. The repo contains a Go API, a React dashboard for managing bots, an embeddable Preact widget, and an evaluation suite that scores answer quality with an LLM judge calibrated against human labels.
- Ingestion: single URLs, sitemap discovery with an approve/reject queue for the pages it finds, bulk URL import, PDF upload and raw text. Pages are fetched with a static crawler (colly) or a headless Chromium pool (go-rod) for JavaScript-heavy sites. Sources can be re-ingested on a schedule.
- Retrieval: sentence-aware chunking (512 tokens, ~15% overlap), OpenAI
text-embedding-3-smallembeddings with deterministic point IDs, and tiered similarity search: high, medium and low confidence tiers decide whether the bot answers normally, answers with a caveat, or falls back. Optional query decomposition for multi-part questions. - Grounded answers: a guarded system prompt, per-bot guardrails and topic rules, and a configurable fallback (static message, restricted "smart" fallback, or escalation to a human).
- Actions: bots can call customer-defined HTTP actions through LLM function calling in an agentic loop, with execution logs.
- Human handoff: visitors can ask for a person and leave an email address. The requests appear in the dashboard.
- Embeddable widget: a Preact bundle rendered in a Shadow DOM, so host-page CSS can't leak in. Theming, suggested questions, feedback buttons, a per-bot domain allowlist and optional HMAC-signed embed tokens.
- Multi-tenant SaaS core: organizations, workspaces and roles; Free, Pro and Ultra plans with quotas, model access and feature gates enforced in one place; Redis-backed rate limiting; an admin panel for users, organizations, plans, queues and system health.
- Analytics: message volume, token usage, source usage and visitor feedback per bot.
- Privacy: KVKK/GDPR-style data export, correction and deletion requests, plus a scheduled retention job.
- Measured quality: a RAG eval suite with retrieval metrics, an LLM judge, judge-versus-human calibration and a regression gate (see below).
Built for the Turkish market. The dashboard is Turkish-first by design: a Turkish UI, Turkish-trained sentence segmentation for chunking, and KVKK data export and deletion. Each chatbot still has its own language (Turkish or English), and the widget's UI follows it, as in the English demo bot above. A full multi-language dashboard is scoped in docs/i18n-plan.md.
The demo data is fictional; make screenshots regenerates these images (see CONTRIBUTING.md).
flowchart LR
subgraph Clients
D["Dashboard<br/>React 19"]
W["Chat widget<br/>Preact, Shadow DOM"]
end
subgraph API["Go API (net/http)"]
MW["Middleware<br/>auth cookies, tenant, plan, rate limit"]
H["Handlers"]
S["Services<br/>chat, chatbots, plans, privacy"]
R["Repositories<br/>squirrel + pgx"]
Q["Source queue<br/>worker pool"]
end
PG[("PostgreSQL")]
RD[("Redis")]
QD[("Qdrant")]
R2[("Cloudflare R2")]
LLM["OpenAI / OpenRouter"]
D -- "REST, HttpOnly cookies" --> MW
W -- "public chat API" --> MW
MW --> H --> S --> R --> PG
MW -. "rate limits" .-> RD
S -- "enqueue ingestion" --> Q
S -. "uploads, exports" .-> R2
subgraph Ingestion["Ingestion pipeline"]
direction TB
I1["Fetch and extract<br/>crawler, headless Chromium,<br/>PDF text via MuPDF"] --> I2["Normalize and chunk<br/>512 tokens, ~15% overlap"] --> I3["Embed<br/>text-embedding-3-small"]
end
Q --> I1
I3 -- "upsert vectors" --> QD
I3 -.-> LLM
subgraph Chat["Chat path"]
direction TB
C1["Embed question"] --> C2["Tiered retrieval<br/>high / medium / low"] --> C3["LLM with tools<br/>agentic loop"] --> C4["Fallback or handoff<br/>when confidence is low"]
end
S --> C1
C2 -- "vector search" --> QD
C3 -.-> LLM
Handlers stay thin and the business logic lives in internal/services. LLM calls go through a circuit breaker. AGENTS.md documents the layering, the middleware order and the background jobs, and docs/system/vector-indexing-pipeline.md walks through ingestion step by step.
sequenceDiagram
autonumber
participant V as Visitor (widget)
participant A as Go API
participant P as PostgreSQL
participant E as OpenAI embeddings
participant Q as Qdrant
participant L as LLM
V->>A: POST /api/v1/public/chatbots/{id}/chat
A->>A: Rate limit, domain allowlist, embed token
A->>P: Load bot and plan limits, reserve token quota
A->>P: Save the visitor's message
A->>E: Embed the question
A->>Q: Similarity search, filtered to this bot
Q-->>A: Scored chunks
A->>A: Pick a confidence tier, fit context to the token budget
A->>L: System prompt, history, context and tools
L-->>A: Answer (after any tool calls)
A->>A: Fallback or handoff if the tier is low
A->>P: Save the answer and its sources, settle quota
A-->>V: JSON answer with sources
| Layer | Technology |
|---|---|
| Backend | Go 1.25, net/http, squirrel query builder over pgx (no ORM) |
| Data | PostgreSQL, Redis 7, Qdrant |
| Object storage | Cloudflare R2 (S3-compatible), optional |
| AI | OpenAI embeddings and chat models, OpenRouter, gobreaker circuit breaker |
| Ingestion | colly, go-rod (headless Chromium), sitemap parser, go-fitz (MuPDF) for PDF text |
| Dashboard | React 19, Vite, TypeScript, TanStack Query, Tailwind CSS, Radix UI |
| Widget | Preact, Vite, TypeScript, Shadow DOM; deployed to Cloudflare Pages |
| Testing | Go testing and testify, Vitest, Testing Library, Playwright, a custom RAG eval harness |
| Ops | Docker, Docker Compose, Caddy, GitHub Actions, golang-migrate |
Prerequisites: Go 1.25+, Node.js 20.19+ with npm, Docker with Compose, and an OpenAI API key.
git clone https://github.com/onurceri/botla.git && cd botla
# 1. Configure (the defaults match the local Docker stack)
cp .env.example .env # then set OPENAI_API_KEY and JWT_SECRET
cp frontend/.env.example frontend/.env
# 2. Start PostgreSQL, Redis and Qdrant; the stack also applies the migrations
make up
# 3. Run the API on :8080
make be-run-no-pdf # or `make be-run` for PDF support (needs CGO and MuPDF)
# 4. In another terminal, install the JS workspaces and start the dashboard and widget
npm ci
npm run dev # dashboard on :5173, widget dev server on :5174Open http://localhost:5173, register, create a chatbot, add a URL or some text, and try it in the playground. To reach the admin panel, promote your account with make admin-promote email=you@example.com.
To put a bot on a site, copy the snippet from the dashboard's deploy tab. It looks like this:
<script src="https://<your-widget-host>/widget.js" data-bot="<chatbot-id>"></script>The backend reads all settings from environment variables, loaded from .env. .env.example lists every option with comments. The ones that matter first:
| Variable | Required | Purpose |
|---|---|---|
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD |
yes | PostgreSQL connection |
REDIS_URL |
yes | Rate limiting; the server won't start without it |
QDRANT_URL, QDRANT_API_KEY |
URL only | Vector database |
JWT_SECRET |
yes | Signs auth tokens; use 32+ random characters |
PORT |
yes | HTTP port |
OPENAI_API_KEY |
for chat | Embeddings and chat models |
OPENROUTER_API_KEY |
no | Route chat models through OpenRouter |
DEFAULT_CHATBOT_MODEL |
no | Fallback chat model (bare id); defaults to gpt-6-luna (GPT-6 Luna) |
CORS_ALLOWED_ORIGINS |
no | Origins allowed to call the API with credentials |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME |
no | Storage for uploads, privacy exports and tokenizer data |
RAG_TOPK, RAG_MAX_CONTEXT_TOKENS, RAG_QUERY_DECOMPOSITION |
no | Retrieval tuning |
GO_ENV |
no | production turns on Secure cookies and TLS to PostgreSQL |
The dashboard and widget take VITE_* variables; see frontend/.env.example and widget/.env.example.
.
├── cmd/
│ ├── server/ # API entrypoint and dependency wiring
│ ├── cli/ # Admin CLI (promote/demote platform admins)
│ └── eval/ # RAG eval runner
├── internal/
│ ├── api/ # Handlers, router, admin guards
│ ├── services/ # Business logic: chat pipeline, plans, privacy, handoff
│ ├── repository/ # SQL data access: interfaces, Postgres implementations, mocks
│ ├── rag/ # Chunking, embeddings, Qdrant client, tiered search, LLM clients
│ ├── processing/ # Source ingestion queue and per-type processors
│ ├── scraper/ # Crawler, sitemap parser, headless browser pool
│ ├── evals/ # Eval framework: dataset, metrics, judge, gate
│ ├── integration/ # Integration tests
│ └── ... # auth, models, pdf, workers, testdb
├── pkg/ # config, middleware, ratelimit, storage, tokenizer, urlutil, logger
├── db/migrations/ # SQL migrations (golang-migrate)
├── api/openapi.yaml # OpenAPI 3 spec
├── evals/ # Eval corpus, golden sets, judge rubrics, baseline
├── frontend/ # React dashboard
├── widget/ # Embeddable Preact widget
└── docs/ # Feature specs and system notes
| Suite | Command | Notes |
|---|---|---|
| Go unit tests | make test-fast |
Repository and service tests use the PostgreSQL from make up |
| Go checks | make ci |
go vet, golangci-lint, then the tests |
| Go integration tests | make test-integration-real |
Starts docker-compose.integration.yml (PostgreSQL, Redis, Qdrant), waits for it to be healthy and runs the suites; make test-integration-down removes it |
| Dashboard | npm run ci --workspace=frontend |
ESLint, tsc, Vitest with coverage |
| Widget | npm run ci --workspace=widget |
tsc, ESLint, Vitest with coverage |
| End-to-end | npm run e2e --workspace=frontendnpm run test:e2e --workspace=widget |
Playwright; start the dashboard dev server or build the widget first |
| RAG evals | make eval-up && make eval |
Calls real models (OPENAI_API_KEY, or OpenRouter); a run costs a few cents |
GitHub Actions runs the Go and web workflows on pull requests and on pushes to main. Real-service integration tests run weekly.
The evals/ suite drives Botla's real ingestion, retrieval and chat code over a fictional 13-document corpus and 45 golden cases (answerable, multi-hop, unanswerable and prompt-injection attacks), plus a 22-case held-out set. It reports retrieval metrics, including how much of the needed context actually reached the prompt, and uses an LLM judge with versioned rubrics for faithfulness, correctness, refusal and injection resistance.
The judge is measured as well. It is calibrated against 21 human-labelled answers (Cohen's kappa 0.90 on the current baseline), checked for consistency across repeats and probed for verbosity bias, and the gate fails if its kappa drops below 0.60. A regression gate compares each run with a recorded baseline using per-slice tolerances, and a GitHub workflow runs it on demand or on pull requests labelled run-evals. The eval README has the results, the multi-hop experiments and the known limitations.
make eval-test # offline unit tests for the eval framework (no API keys)
make eval-quick # single-repeat smoke run against real modelsPlease report vulnerabilities privately; SECURITY.md explains how and what is in scope. Built-in protections include HttpOnly cookie auth with refresh-token rotation, tenant scoping on authenticated routes, SSRF protection for URL ingestion, per-bot domain allowlists and signed embed tokens for the widget, security headers, and plan-aware rate limiting.
See CONTRIBUTING.md for setup, the checks to run and the commit style. AGENTS.md and the per-area AGENTS.md files describe the codebase conventions.
Copyright (C) 2025-2026 Onur Ceri.
Botla is free software under the GNU Affero General Public License v3.0 or later. If you run a modified version as a network service, the AGPL requires you to offer that version's source code to its users.






