RAG-as-a-service. A tenant signs up, creates a knowledge base, uploads docs (PDF / Markdown / txt), and a background worker chunks + embeds them into pgvector. The tenant gets a public API key and an embeddable chat widget to drop on their own site — end-users ask questions and get answers grounded in that tenant's docs, with citations. Queries are metered and billed through Stripe.
Multi-tenant SaaS built to show real full-stack range: own JWT auth, row-level tenancy, a real RAG pipeline, background jobs, Stripe metered billing, an embeddable widget, Docker, and CI — not tutorial CRUD.
Browser ─┬─ dashboard ──────────► web (Next.js, App Router) ← only host port (WEB_PORT)
└─ widget (any site) ──► │
│ /api/backend/* (proxy)
▼
api (FastAPI, async SQLAlchemy)
│ │
┌────────┘ └────────┐
▼ ▼
postgres 17 + pgvector redis ──► worker (arq)
tenants · users · kbs · ingest: extract →
documents · chunks(vector) · chunk → embed → store
api_keys · subscriptions · usage_ledger
- Ingestion is async (worker): upload → extract → chunk → embed →
ready. - Query is inline (api): embed question → cosine-NN over the KB's chunks → grounded generation → citations → usage row.
- Row-level multi-tenancy: every row carries
tenant_id; requests scope through acurrent_tenantdependency. - Provider seam: OpenAI in prod, a deterministic offline
fakeprovider for tests/CI — so the suite runs with no network and no keys.
| Layer | Choice |
|---|---|
| Backend | Python 3.12 · FastAPI · SQLAlchemy 2 (async, psycopg3) · Alembic |
| Vector store | Postgres 17 + pgvector (IVFFlat cosine index) |
| Jobs | Redis + arq worker |
| Auth | Own JWT access/refresh · argon2 password hashing |
| LLM | OpenAI (embeddings + generation) behind a Provider interface |
| Billing | Stripe metered (test mode) — Checkout + signature-verified webhook |
| Frontend | Next.js (App Router) + Tailwind |
| Widget | Vanilla JS, self-contained bundle |
| Infra | Docker Compose · Makefile · GitHub Actions CI |
cp .env.example .env
# set JWT_SECRET (openssl rand -hex 32); add OPENAI_API_KEY for the live demo
make up # build + start + migrate
# dashboard: http://localhost:3000 (or point portless at the web container)OPENAI_API_KEY— required for the live demo (real embeddings + generation). Without it, setLLM_PROVIDER=fakefor a deterministic offline run.- Stripe test keys (
STRIPE_ENABLED=true+STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRICE_ID) — required only for the live billing demo. Billing code is real; the test suite mocks the Stripe client, so CI needs no keys.
Signup → create KB → upload a doc → worker embeds it → dashboard shows the embed
snippet + API key → open demo/index.html with the widget → ask a question → get a
grounded answer with a citation. Full walkthrough:
docs/runbooks/local-dev.md.
make test # backend suite (LLM + Stripe mocked)
make lint # ruff + mypy
make seed # demo tenant + KB + API key
make # list all commandsDocs: docs/CLAUDE.md · Architecture decisions:
docs/decisions/0001-architecture.md.