Teams upload their documents and chat with an assistant that answers only from those documents, with citations — Retrieval-Augmented Generation (RAG) as a real, billable product.
🌐 Live demo · one-click Deploy your own
Free tier — the first request may take ~50s to wake the services.
ragdesk is built the way production AI software actually ships: a strongly-typed Go core for tenancy, billing and metering; a Python/FastAPI service for the LLM and embedding pipeline; a Next.js front-end; Postgres + pgvector for rows and vectors; and a provider-agnostic model layer that runs on a free local LLM (Ollama) or a free hosted tier (Gemini, Groq). Every feature shipped in small, tested, atomically-committed phases — CI, CD and CodeQL green — on $0 of paid infrastructure.
Ask your documents — grounded answers, with citations:
Plans, metered usage and one-click upgrade — billing as a first-class feature:
| Landing | Workspaces dashboard — multi-tenant |
|---|---|
![]() |
![]() |
| Documents — async ingestion → embeddings | Create account |
|---|---|
![]() |
![]() |
- 🏢 Multi-tenant workspaces — organizations, members, roles, hard data isolation
- 🔐 JWT auth — register/login with bcrypt-hashed passwords, HS256 tokens
- 📄 Document ingestion — upload → chunk → embed (Ollama) →
pgvector, processed async via a Redis queue + worker - 💬 RAG chat — streaming answers grounded in your documents, with citations (pgvector cosine retrieval)
- 🔌 Provider-agnostic LLM — mix & match embeddings and chat: Ollama (local/$0), Gemini & Groq (free hosted tiers), or a deterministic fake for tests/CI
- 💳 Billing & metering — Stripe subscriptions (test mode), per-workspace usage metering, plan limits enforced with
402 Payment Required; runs $0 with a dev-mode fallback - 🔒 Production hardening — rate limiting, structured logs, health probes, govulncheck, CodeQL
- 🔭 Observability — optional OpenTelemetry tracing across web → API → AI → Postgres (no-op until configured)
- 🐳 Cloud-native — multi-stage Docker images,
docker compose up, GitHub Actions CI + CD (images to GHCR)
flowchart LR
User([User]) --> Web[web · Next.js 16 + TS]
Web -->|REST / SSE| API[api · Go + chi]
API --> PG[(Postgres + pgvector)]
API --> Redis[(Redis · cache + queue)]
API -->|ingest / chat| AI[ai · Python + FastAPI]
AI --> PG
AI -->|provider-agnostic| LLM{{Ollama · Gemini · Groq}}
See docs/architecture.md for the full design.
| Layer | Choice |
|---|---|
| Frontend | Next.js 16, TypeScript, Tailwind v4 |
| Core API | Go 1.26, chi, pgx, go-redis, JWT, bcrypt |
| AI service | Python, FastAPI, pgvector; Ollama · Gemini · Groq |
| Data | PostgreSQL 16 + pgvector, Redis 7 |
| Billing | Stripe (test mode) |
| Observability | OpenTelemetry (OTLP), Jaeger |
| Infra | Docker (multi-stage, distroless), docker-compose, GitHub Actions (CI + CD → GHCR), CodeQL |
git clone https://github.com/thefcan/ragdesk.git
cd ragdesk
cp .env.example .env
# Backend: Postgres (pgvector), Redis, the Go API and the Python AI service
make up # docker compose up --build -d
# Frontend (Next.js) — in another terminal
cd web && npm install && npm run dev # http://localhost:3000Try the API directly:
# register (bootstraps a default workspace) and call an authenticated endpoint
TOKEN=$(curl -s -X POST localhost:8080/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"supersecret"}' | jq -r .token)
curl -s localhost:8080/workspaces -H "Authorization: Bearer $TOKEN"Browse it interactively in Swagger UI at /docs (raw spec at /openapi.yaml).
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register |
public | Create a user + default workspace, returns a JWT |
| POST | /auth/login |
public | Exchange credentials for a JWT |
| GET | /workspaces |
Bearer | List the caller's workspaces |
| POST | /workspaces |
Bearer | Create a workspace |
| GET | /workspaces/{id} |
Bearer | Get a workspace (members only) |
| GET | /workspaces/{id}/members |
Bearer | List members |
| POST | /workspaces/{id}/members |
Bearer | Add a member (owner/admin) |
| GET | /workspaces/{id}/documents |
Bearer | List a workspace's documents |
| POST | /workspaces/{id}/documents |
Bearer | Upload a document (async ingestion) |
| POST | /workspaces/{id}/chat |
Bearer | Ask a question — streaming RAG answer with citations |
| GET | /workspaces/{id}/billing |
Bearer | Plan, limits and current-period usage |
| POST | /workspaces/{id}/billing/checkout |
Bearer (owner) | Start an upgrade (Stripe checkout, or dev confirm) |
| POST | /workspaces/{id}/billing/portal |
Bearer (owner) | Open the Stripe billing portal (manage / cancel) |
| POST | /billing/webhook |
Stripe-signed | Apply subscription changes from verified events (idempotent) |
| GET | /healthz · /readyz · /version · /metrics |
public | Probes, build info & Prometheus metrics |
Billing is modelled per workspace (the tenant). Plans and their limits live in code; usage is metered durably in Postgres and enforced at the API edge.
| Plan | Documents | Chat messages / month | Price |
|---|---|---|---|
| Free | 25 | 100 | $0 |
| Pro | 1,000 | 5,000 | $29 / mo |
- Metering — a
usage_counterstable buckets usage per(workspace, month, metric)and is incremented atomically (INSERT … ON CONFLICT … DO UPDATE). Documents are counted directly. - Enforcement — over-limit document uploads and chat messages are rejected with
402 Payment Requiredbefore any expensive work runs. The document cap is applied inside the insert so concurrent uploads can't slip past it. - Provider-agnostic payments — a
billing.Providerinterface mirrors the LLM layer. With Stripe test-mode keys it creates a hosted Checkout Session and fulfils the upgrade from a signature-verified webhook. With no keys it runs a $0 dev mode: a local dev-confirm endpoint stands in for the webhook, so the whole flow is demoable for free. - Self-serve management — owners upgrade, and manage or cancel through the Stripe billing portal (a local cancel stands in when running $0).
- Safe by default — the webhook is signature-verified (forged events are rejected) and idempotent (Stripe delivers at least once; processed event ids are recorded and skipped), the API refuses to start with a Stripe key but no webhook secret, and only a workspace owner can change the plan.
Every push to main builds and publishes both service images to GHCR
(ghcr.io/thefcan/ragdesk-api, ghcr.io/thefcan/ragdesk-ai), so the stack runs
on any container host. A one-click Render Blueprint (render.yaml)
provisions Postgres + pgvector, Redis, the API, the AI service and the web app on
the free tier — Postgres, Redis and the shared secrets are wired automatically.
See docs/deploy.md for the full guide (Render, Fly.io,
Vercel, or your own docker compose).
Both services emit OpenTelemetry traces — a no-op until
OTEL_EXPORTER_OTLP_ENDPOINT is set, so the default run pays nothing. The Go API
instruments inbound HTTP, the outbound call to the AI service, and Postgres
queries; the Python service continues the same trace through FastAPI and its DB
calls. A request shows up as one connected trace: web → API → AI → Postgres.
# View traces locally in Jaeger (http://localhost:16686)
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 docker compose --profile observability upThe API also exposes Prometheus metrics at /metrics — per-route request
rate, latency (a histogram) and error counts, plus Go runtime and process
metrics — ready to scrape with no extra setup.
Every component has a free path: a local LLM (Ollama) or free hosted tiers (Gemini, Groq), Postgres+pgvector and Redis in Docker, Stripe test mode, Render/Vercel free tiers, and GitHub Actions for public repos.
MIT © 2026 Furkan Can Karafil





