api/— Python/FastAPI backend (Docling ingestion + Turbopuffer vector search)sdks/typescript/— TypeScript SDK (@bigrag/client)sdks/python/— Python SDK (bigrag)app/— admin UI (Vite + TanStack Router + Tailwind v4 + Base UI,@bigrag/app)website/— Documentation site (Next.js + Fumadocs, content inwebsite/content/docs/)
All coding guidelines, patterns, and conventions are documented in STYLEGUIDE.md. Follow the rules and patterns defined there.
Don't write comments or docstrings in code under api/bigrag/, sdks/typescript/src/, sdks/python/src/, app/, or website/. This includes #, //, /* */, /** */ JSDoc, and Python """docstrings""". The diff and well-named identifiers should speak for themselves; surprising invariants belong in commit messages or PR descriptions, not in the code. The only allowed exceptions are functional directives — shebangs, # type: ignore, # noqa, # ruff:, // @ts-…, // biome-ignore, // eslint-…, and similar tool pragmas, plus Pydantic Field(description="...") strings that are load-bearing for OpenAPI.
If you find yourself wanting to explain code, rename or restructure it instead.
The repo follows aggressive package-style splits. If a single file grows past ~300 LoC, look for a clean seam (per-handler, per-provider, per-stage) and split into a package directory. Examples already shipped:
api/bigrag/db/models/(per domain: auth, collection, connector, document, instance, observability, preference, webhook)api/bigrag/app_factory/(lifespan, exception_handlers, routers)api/bigrag/mcp/(tools, unscoped, scoped, cli)api/bigrag/services/{embedding,retrieval,webhook,vector_store,storage,url_security,access_log,event_bus,queue_conversion,queue_embedding,chat,runtime_setting_specs}/packagessdks/python/src/bigrag/resources/admin/andsdks/typescript/src/resources/admin/(settings, users, api_keys, access, audit, connectors, embedding_presets, mcp_servers, vector_storage)
When adding new code, prefer the smallest meaningful module instead of dropping it into the nearest catch-all.
- Backend: Python 3.12+, FastAPI, SQLAlchemy 2 (async) + asyncpg, Alembic, docling, openai, cohere, cryptography (Fernet for at-rest encryption of provider secrets), dramatiq (Redis broker)
- Vector DB: Turbopuffer
- Metadata DB: PostgreSQL 17
- Ingestion: Docling (PDF, DOCX, PPTX, XLSX, HTML, Markdown, images)
- Embedding: OpenAI, Cohere, Voyage, and OpenAI-compatible providers
- Caching/queues: Redis (auth principal cache, embedding cache, dramatiq ingestion + webhook queues)
- Python backend:
uv(lockfile atapi/uv.lock) - Python SDK:
uv(lockfile atsdks/python/uv.lock) - TypeScript SDK + Website + App:
pnpmworkspaces (rootpnpm-workspace.yaml)
Gotcha: pnpm-workspace.yaml sets minimumReleaseAge: 10080 (7 days). Any pnpm install that triggers a fresh dependency resolution may fail for packages published in the last week (commonly rolldown, rollup, and @tanstack platform binaries). Either wait, or add the offending name to minimumReleaseAgeExclude:.
- Python:
ruff(config inapi/pyproject.toml) - TypeScript/JS:
biome(config inbiome.jsonc)
Always run lint + format before committing. Either let the pre-commit hook run them, or invoke them manually — never commit unformatted code:
# Python (api/)
cd api && uv run ruff check --fix .
cd api && uv run ruff format .
# Python SDK
uv run --project api ruff check --fix sdks/python/src
uv run --project api ruff format sdks/python/src
# TS / JS / JSON / CSS (everything else)
pnpm exec biome check --write .The repo ships a .pre-commit-config.yaml that runs ruff check, ruff format, and biome check on staged files. Install it once per clone:
uv tool install pre-commit # or: brew install pre-commit
pre-commit installAfter that, every git commit runs the same formatters for API Python and TS/JS/CSS files that CI enforces. Run package build/typecheck commands manually when touching SDKs. NEVER skip hooks with --no-verify. If a hook auto-fixes a file, the commit aborts — re-stage and commit again.
The old package-level unit/integration suites, end-to-end suites, and coverage commands have been removed. Do not add package test runners, end-to-end test runners, or coverage requirements back to feature work unless the project reintroduces them deliberately.
Use lint, typecheck, build, compile, and runtime smoke checks for current verification. Keep website/content/docs/development/testing.mdx in sync when verification guidance changes, and do not commit generated coverage artifacts.
- Backend uses FastAPI dependency injection via
bigrag/db/session.py::get_sessionandbigrag.middleware.auth::get_current_user/require_admin_session. - Database layer lives in
bigrag/db/:engine.py(async engine),session.py(FastAPIget_sessiondependency),models/(ORM models, split by domain),bootstrap.py(stamp-or-upgrade on startup). Schema changes go through Alembic (api/alembic/). Migrations are collapsed into a single0001_initial_schema.py— re-emit (do not stack new revisions) when models change, and runalembic revision --autogenerate -m drift_checkto confirm no drift. - The user-session ORM model is
UserSession(tablesessions) — notSession, which would clash with SQLAlchemy's own session class. - App factory:
api/bigrag/app_factory/{lifespan,exception_handlers,routers}.pyassembled bybigrag/main.py::create_app(). - Services are organized by domain under
api/bigrag/services/— most non-trivial ones are packages (embedding/,retrieval/,webhook/,vector_store/,storage/,url_security/,access_log/,event_bus/,queue_conversion/,queue_embedding/,chat/,runtime_setting_specs/). - Tenant scoping: route handlers call
enforce_collection_pin(user, collection_name)fromrouters/__init__.pyto honor pinned API keys. Connector accounts/sources carry an optionaltenant_idcolumn. - Background workers: Dramatiq actors live in
services/jobs/. The ingestion queue (services/queue.py) drains viabigrag-worker. - MCP server:
api/bigrag/mcp/package (tools,unscoped,scoped,cli); entry pointbigrag-mcp = "bigrag.mcp:cli". - SDK uses resource namespaces:
client.collections.list(),client.documents.upload(),client.admin.users.list()etc. Theadminresource is itself a package — sub-resources live inresources/admin/{settings,users,api_keys,access,audit,connectors,embedding_presets,mcp_servers,vector_storage}.py. - SDK reliability: both Python and TypeScript SDKs send
Idempotency-Key: <uuid4>on POST/PUT/PATCH/DELETE and only retry mutating calls when an idempotency key is present. Both expose typed error subclasses (BadRequestError,ConflictError,PayloadTooLargeError,UnprocessableEntityError,BadGatewayError,ServiceUnavailableError, etc.).
- Errors to clients: never
detail=str(exc)— usesafe_error_detail(exc, fallback)fromservices/error_sanitize.py. For persisted error messages (Document.error_message,ConnectorSource.last_error, SSE error frames), usesanitize_message_text(...)so secrets/URLs/exception internals never leak. uuid_or_404(value, label),decode_cursor_or_400(cursor),ensure_embedding_or_400(collection),verify_or_422(...),is_mcp_key(key),enforce_collection_pin(user, name)are canonical helpers — use them instead of inlining the patterns.- CHECK constraints go in
__table_args__, not inline onmapped_column(sa.CheckConstraint(...)), so Alembic autogenerate picks them up. - No
routers → services → routersimports. Helpers that services need live inapi/bigrag/services/, notapi/bigrag/routers/.
Always update the docs site when making any code change. This includes adding, updating, or removing features, endpoints, models, config options, SDK methods, or CLI flags. The docs live in website/content/docs/ as .mdx files.
Pages to keep in sync:
api-reference/— endpoint signatures, request/response schemas, error codesconcepts/— feature explanations, examples, and curl snippetssdks/typescript.mdx,sdks/python.mdx,sdks/mcp.mdx— SDK method signatures and usagegetting-started/configuration.mdx— environment variables and TOML optionsdeployment/— Docker Compose snippets and production settingscomparison.mdx— if a new feature is a differentiator vs competitors
If a feature is removed, remove it from the docs too. Never leave stale references.
./dev.sh # starts infra + backend + worker
./dev.sh --infra # postgres + redis only
./dev.sh --website # docs site only
pnpm dev:app # admin UI on localhost:3000- admin UI: http://localhost:3000 (first run →
/setupto create admin) - Backend API: http://localhost:4000 (Swagger docs at /docs)
- Postgres: localhost:5432
- Redis: localhost:6379
dev.sh only tears down the docker stack that it started — if you ran docker compose up separately before invoking dev.sh, it leaves that running on exit.
The bigrag-mcp entry point (api/bigrag/mcp/cli.py) wraps the REST API as MCP tools for Claude Desktop / Cursor / etc. It's an HTTP client, not an in-process bolt-on — auth is via BIGRAG_API_KEY. Pinned (collection-scoped) and unscoped tool surfaces live in api/bigrag/mcp/scoped.py and api/bigrag/mcp/unscoped.py; shared call helpers + HTTP plumbing in api/bigrag/mcp/tools.py. When adding or renaming an API endpoint that retrieval clients care about, update the matching tool surface and the docs at website/content/docs/sdks/mcp.mdx.
Auth is admin-account + session cookie (admin UI) or minted API keys (bigrag_sk_..., external clients). First admin is created via the admin UI's /setup page; subsequent admins via /users; API keys via /api-keys. There is no shared-secret env var — do not introduce one.
API keys can be pinned to a single collection (permissions.collection). Route handlers MUST call enforce_collection_pin(user, collection_name) for collection-scoped operations so a pinned key sees a 404 for any other collection. The middleware-level enforce_collection_scope runs first, but the route-level helper is defense-in-depth.
When adding a feature, also clean up what's left from the previous shape: stale imports, dead branches, comments-that-explain-code, single-callsite helpers that should be inlined, and constants/aliases nobody reads. The "no comments" + "package-style splits" + "no routers → services → routers" rules above only stay true if every change actively maintains them.