A self-hosted RAG service with hybrid search (semantic + BM25), admin dashboard, and Claude Code plugin for agent integration.
rag-service
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Adapter │──>│ Screening│──>│ Chunk + Embed │ │
│ │ (json,md)│ │ + Redact │ │ (configurable API)│ │
│ └─────────┘ └──────────┘ └────────┬───────────┘ │
│ ^ │ │
│ │ --input / --dry-run │ │
│ │ --dry-run │ │
│ v │
│ ┌──────────────────────────────────────────────┐ │
│ │ Direct PostgreSQL + pgvector │ │
│ │ │ │
│ │ kb_chunks │ │
│ │ ┌─────┬──────┬───────────┬──────────┐ │ │
│ │ │ id │ text │ embedding │ source/tags│ │ │
│ │ └─────┴──────┴───────────┴──────────┘ │ │
│ │ │ │
│ │ ACL, audit log, feedback, and query signals │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ v v │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ BM25 search │ │ Semantic search │ │
│ │ (Intl.Segmenter │ │ (pgvector cosine)│ │
│ │ + Porter stem) │ └────────┬─────────┘ │
│ └────────┬────────┘ │ │
│ └──────────┬──────────┘ │
│ v │
│ ┌─────────────────┐ │
│ │ RRF Fusion │ │
│ │ (keyword 0.25 + │ │
│ │ semantic 0.75) │ │
│ └────────┬────────┘ │
│ v │
│ ┌────────────────────────────────────────────┐ │
│ │ /api/query → chunks + optional LLM RAG │ │
│ │ /api/learn → add chunk (unverified) │ │
│ │ /api/report → flag content issue │ │
│ │ /dashboard → admin UI │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Agent Curation Loop
┌───────────────────────────────────────┐
│ │
│ search ──> learn/report │
│ │ │
│ v │
│ triage ──> inspect ──> edit/verify │
│ │ │
│ v │
│ curate (walks queue item-by-item) │
│ │ │
│ v │
│ improved KB ──> better search ──> │
└───────────────────────────────────────┘
Go to Zeabur Dashboard and create a new project.
The Marketplace template provisions pgvector/pgvector automatically. For a
manual deployment, provide a PostgreSQL database where the service account can
run CREATE EXTENSION vector and schema migrations.
Provide an OpenAI-compatible embedding endpoint and key. Answer generation is optional and uses an OpenAI-compatible chat-completions endpoint.
EMBEDDING_API_KEY(orZEABUR_AI_HUB_API_KEY)EMBEDDING_API_URL(optional; defaults tohttps://api.openai.com/v1)LLM_API_KEY/LLM_API_URLfor RAG answers and optional LLM features
In your project, click Add Service → Marketplace, search for RAG Service, and fill in:
- The PostgreSQL connection is injected automatically by the template
- A Zeabur AI Hub key for answer generation
- A Gemini API key for the Marketplace template's default embedding provider, or credentials for another OpenAI-compatible embedding provider when deploying manually
Schema migrations run automatically on first startup — no manual setup needed.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/query |
POST | API key | Search knowledge base + optional RAG answer |
/api/learn |
POST | API key | Add new knowledge chunks |
/api/report |
POST | API key | Report content issues |
/api/feedback |
POST | API key | Submit result feedback |
/api/admin/* |
GET/POST | API key (admin) | API key management, signals, reports, chunks |
/dashboard |
GET | Basic Auth | Admin dashboard |
curl -X POST "https://your-rag-service/api/query" \
-H "Authorization: Bearer $RAG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "how to deploy", "mode": "hybrid", "top_k": 5}'Search modes are hybrid (default), semantic, bm25, and sql-hybrid.
Hybrid mode retrieves a wider BM25 and semantic candidate pool, combines ranks
with RRF, applies optional per-source weights and recency decay, and can
optionally send the candidate pool through an LLM reranker.
Deployment-specific search tuning stays in environment variables rather than source code:
RAG_QUERY_EXPANSIONS='[{"match":"object storage","append":"s3 bucket"}]'
RAG_SOURCE_WEIGHTS='{"docs":1.0,"learned":0.8}'curl -X POST "https://your-rag-service/api/learn" \
-H "Authorization: Bearer $RAG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Deploy with Docker", "content": "Steps to deploy..."}'Add the marketplace and install the plugin:
claude plugin marketplace add zeabur/rag-service && claude plugin install rag-service@rag-serviceThen configure the connection:
claude
# Inside Claude Code, run:
/zeabur-rag-setupThis sets ZEABUR_RAG_URL and RAG_API_KEY in your Claude Code settings. The agent will automatically use the search skill when answering questions.
The plugin ships 8 skills split into two tiers: everyday use, and maintenance of the knowledge base itself.
Using the knowledge base — any agent with an API key can invoke these:
| Skill | When it triggers |
|---|---|
zeabur-rag-setup |
First-time setup — asks for ZEABUR_RAG_URL and RAG_API_KEY, writes them to Claude Code settings |
zeabur-rag-search |
Any technical question that might be answered by the KB (fires automatically, even without an explicit "search" instruction) |
zeabur-rag-learn |
After solving a problem or discovering something the KB didn't cover — contributes a new chunk marked unverified until an admin reviews it |
zeabur-rag-report |
A search result looks wrong, outdated, or a topic is missing — files an issue against a chunk without needing write access |
Curating the knowledge base — admin-scope skills that form a self-improving loop:
| Skill | Role in the loop |
|---|---|
zeabur-rag-triage |
Lists everything pending: open reports, unverified learn chunks, low-similarity queries, negative feedback |
zeabur-rag-inspect |
Pulls one chunk's full content + related reports + search signals + edit history — the "look before you touch" step |
zeabur-rag-edit |
Patches an existing chunk in place (title, content, tags, status). Requires admin scope |
zeabur-rag-curate |
Orchestrator — walks through every triage item one by one with the user, deciding fix / reject / learn-new for each |
Every agent interaction feeds a signal back into the KB, and the curation skills close the loop:
- Agents run
search→ every query is logged with its top similarity score. Low-similarity queries (< 0.4) become signals that the KB may have a gap. - Agents run
learn/report→ new unverified chunks and open reports pile up in a queue. - An admin runs
curate(ortriagefor a quick read-only view) → the skill walks the queue item-by-item:- For a report:
inspectthe chunk →editto fix, orlearnnew content if it's a gap → close the report. - For an unverified
learnchunk:inspect→ verify, reject, or edit before verifying. - For a low-similarity query:
searchto reproduce →learnnew content if it's a true gap, oreditan existing chunk to rank better. - For negative feedback:
inspectthe returned chunks → fix whichever one misled the agent.
- For a report:
- Verified chunks rank at full weight; rejected ones are removed. The next round of searches sees an improved KB.
Agents and admins operate on the same data — the skills just expose different capabilities depending on scope. Point the plugin at any RAG service deployment and the loop works out of the box.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Direct PostgreSQL connection string; the database must support pgvector |
EMBEDDING_API_KEY |
Yes* | Embedding provider key; falls back to LLM_API_KEY or ZEABUR_AI_HUB_API_KEY |
EMBEDDING_API_URL |
No | OpenAI-compatible embedding base URL |
EMBEDDING_MODEL |
No | Embedding model (default: text-embedding-3-small) |
EMBEDDING_DIMENSIONS |
No | Vector dimension shared by the model and pgvector schema (default: 1536) |
LLM_API_KEY / LLM_API_URL |
No | Required only for generated answers, rewriting, LLM screening, or reranking |
ZEABUR_AI_HUB_API_KEY |
No | May be used as the embedding and LLM key on Zeabur |
RAG_API_KEY |
Auto | API key for service access (auto-generated on Zeabur) |
RAG_BASIC_AUTH |
Auto | Dashboard auth (format: admin:password) |
RAG_MODEL |
No | LLM model (default: gemini-2.5-flash-lite) |
CORS_ORIGIN |
No | CORS origin (default: *) |
RAG_QUERY_EXPANSIONS |
No | JSON array of literal query expansion rules |
RAG_SOURCE_WEIGHTS |
No | JSON object mapping source names to ranking multipliers |
RERANK_API_URL / RERANK_API_KEY / RERANK_MODEL |
No | Optional second-stage reranker configuration |
SCREENER_ENABLED |
No | Enable regex ingestion screening (default: true) |
SCREENER_LLM_ENABLED |
No | Opt in to sending chunks through the LLM sensitivity review (default: false) |
SCREENER_CUSTOM_RULES_FILE |
No | Path to deployment-private screening rules; keep this file outside the public repository |
See .env.example for a complete template.
The default is text-embedding-3-small with 1536 dimensions through an
OpenAI-compatible endpoint. EMBEDDING_MODEL and EMBEDDING_DIMENSIONS are
configurable, but existing vectors must be re-embedded before changing the
dimension. Startup rejects a mismatched pgvector column instead of silently
mixing incompatible embeddings.
This model is used for:
- Query-time embedding — vectorizing search queries (
src/embedding.ts) - Batch embedding — vectorizing chunks during pipeline upload (
src/embedding.ts)
cp .env.example .env
# Fill in your credentials
bun install
bun run startImport data into the knowledge base using adapters.
Before embedding or upload, imported chunks pass through a sensitive-content
screen. Known secrets are rejected; personal email addresses, phone numbers,
private IPs, internal hostnames, and credential values are masked. This also
applies to --dry-run output. The optional LLM phase can enforce private
organization rules without committing those rules to this repository.
# Import from a JSON file
bun run pipeline --adapter json --input data/my-chunks.json
# Import from a directory of Markdown files
bun run pipeline --adapter markdown --input ./docs/
# List available adapters
bun run pipeline --list| Option | Description |
|---|---|
--adapter <name> |
Run specific adapter(s). Repeat for multiple. Omit for all. |
--input <path> |
Set INPUT_PATH in adapter config. |
--replace |
Delete all chunks before import (full rebuild). |
--dry-run |
Export chunks as JSON to stdout; skip embed/upload. |
--list |
List available adapters and exit. |
--help |
Show help. |
Create a .ts file that exports a SourceAdapter:
import type { SourceAdapter, Chunk } from "rag-service/pipeline/types";
export default {
name: "my-source",
description: "Import from my custom source",
async export(config) {
// config contains all env vars + CLI --input as INPUT_PATH
const chunks: Chunk[] = [];
// ... fetch data, chunk it, push to chunks[] ...
return chunks;
},
// Optional: called after successful upload with the uploaded chunks
async afterUpload(chunks, config) {
// e.g. write back chunk IDs to source files
},
} satisfies SourceAdapter;Load external adapters by setting RAG_ADAPTERS_PATH:
RAG_ADAPTERS_PATH=./my-adapters bun run pipelinejson — Import from a JSON file. Accepts Chunk[] or arrays of objects with id + content fields.
markdown — Scan a directory for .md/.mdx files, parse frontmatter, split by ## headings (~800 chars per chunk).