RAG platform for educational use at DSV, Stockholm University. Rust/Axum backend, React frontend, Qdrant vector search.
See the codeRAG platform for educational use at DSV, Stockholm University. Teachers upload course materials; students get an AI assistant grounded in those documents, with safeguards designed to support learning.

simple (one-shot retrieve-then-answer) and FLARE (multi-turn, logprob-triggered mid-stream retrieval). The legacy parallel strategy was retired in favour of the tool-use axis below.tool_use_enabled toggle on each course. When on, generation splits into a hidden-thinking research phase (model calls keyword_search / RAG seed / KG-expansion tools, with FLARE's logprob signal injected as a tool event) followed by a clean writeup phase. Per-tool expandable results, research thinking, and a research/writeup token-split are persisted alongside the message and rendered above the assistant bubble.[n] badges (accepts naked-digit and filename-form variants); a right-rail sources panel shows what was actually cited, with a count button that resets the uncited-override on fresh-open.gpt-oss-120b and cross-linked with part_of_unit / solution_of / prerequisite_of / applied_in edges. Retrieval expands top-k along the graph. Gated by the course_kg feature flag.gpt-oss-120b, JSON-schema-strict, reasoning_effort: low) that returns 0..=2 tagged suggestions for the draft the student is typing, severity-coloured against an 8-kind CLEAR-grounded rubric, with a Beginner/Expert calibration toggle. Soft-blocks Send when suggestions are present; Use ideas rewrites the draft via gpt-oss-120b. Mobile drawer + tablet support; per-iteration history persisted for export. Gated by the aegis feature flag.gpt-oss-120b, low effort) + Socratic rewriter (gpt-oss-120b), with KG-driven multi-turn proximity tracking. Gated by the extraction_guard feature flag.Teacher section beside Admin showing the teacher's own spend against their daily cap: today, a rolling window, per course, per provider and per day, including the ingest / classification spend that has no student behind it. When the cap is too low it drafts the increase request (identity, the courses it covers, current limit, busiest day) for lambda@dsv.su.se. The onboarding guide is its second tab.mod_lua; attribute-based role auto-promotion rules.ext: users, in-app data-handling ack, English + Swedish, WCAG 2.1 AA fixes.Detail figures for the document-ingest and chat/RAG pipelines (including the FLARE multi-turn loop): docs/ARCHITECTURE.md.
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() |
Regenerate with docs/screenshots/regenerate.mjs (see docs/screenshots/README.md).
| Layer | Technology |
|---|---|
| Backend | Rust (Axum, SQLx, Tokio) |
| Frontend | React 19, TypeScript 6, Vite, TanStack Router/Query, Tailwind 4, react-force-graph-2d, i18next 26 |
| Frontend runtime | Node 26 (Alpine) in Docker |
| Database | PostgreSQL 16 |
| Vector DB | Qdrant (per-course versioned collections) |
| LLM | Admin-managed chat_models catalog (per-model provider + USD rates). Providers register from env keys: Cerebras (default; gpt-oss-120b across the stack ; classifiers, Aegis, rewrites, writeup), OpenAI, Anthropic, Groq, Gemini, or any OpenAI-compatible endpoint via MINERVA_LLM_BASE_URL__<PROVIDER> |
| Embeddings | OpenAI or local fastembed (memory-budgeted LRU cache, HuggingFace cache persisted on /data0 in prod) |
| Edge | Apache 2 with mod_shib + mod_lua |
cp .env.example .env # add CEREBRAS_API_KEY, OPENAI_API_KEY
docker compose up
Backend on :3000, frontend dev on :5173. With MINERVA_DEV_MODE=true (compose default) Shibboleth is bypassed; the backend reads X-Dev-User and falls back to the first admin in MINERVA_ADMINS.
Production:
docker compose -f docker-compose.prod.yml up -d
# or
docker pull ghcr.io/edwinexd/minerva:master
For the k3s production layout used at DSV, see k8s/.
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string |
QDRANT_URL | Qdrant gRPC endpoint |
MINERVA_HMAC_SECRET | Signs embed/invite/LTI tokens; mirrored to Apache for mod_lua |
MINERVA_ADMINS | Comma-separated admin eppn prefixes |
MINERVA_DOCS_PATH | Document storage path |
CEREBRAS_API_KEY | Inference key; required (the default chat + utility models are Cerebras-hosted) |
OPENAI_API_KEY | OpenAI chat models and OpenAI embeddings (optional with fastembed) |
ANTHROPIC_API_KEY, GROQ_API_KEY, GEMINI_API_KEY | Optional; a provider is registered only when its key is set |
MINERVA_LLM_BASE_URL__<PROVIDER> | Overrides a provider's base URL (self-hosted or proxied endpoints) |
MINERVA_EMBEDDER_URL, MINERVA_RERANKER_URL | gRPC endpoints of the model servers |
MINERVA_BASE_URL | Public base URL for LTI tool URLs |
MINERVA_LTI_KEY_SEED | RSA seed for LTI 1.3 (falls back to HMAC secret) |
MINERVA_SERVICE_API_KEY | Bearer for /api/service/* pipelines |
MINERVA_DEV_MODE | true bypasses Shibboleth |
MINERVA_DEFAULT_COURSE_DAILY_USD | Per-student-per-course spend default (0 = unlimited) |
MINERVA_DEFAULT_OWNER_DAILY_USD | Per-owner aggregate spend default (0 = unlimited) |
MINERVA_CANVAS_AUTO_SYNC_INTERVAL_HOURS | Canvas re-sync interval |
See .env.example for the rest.
| Path prefix | Auth | Why |
|---|---|---|
/api/integration/* | Per-course API key | Moodle server-to-server |
/api/service/* | Global service API key | Automated pipelines |
/api/embed/*, /embed/* | HMAC-signed embed token | Iframe chat |
/lti/* | LTI 1.3 (OIDC + JWT) | LMS-driven login |
/api/external-auth/* | HMAC-signed invite token | External-auth callback |
/embedding-catalog | Public read-only | Teacher feed of enabled models |
| everything else | Shibboleth | Default |
See apache/README.md for the vhost.
Per DSV-IT policy, new sites must meet the accessibility law; Minerva targets WCAG 2.2 level AA. Compliance is enforced automatically (pre-commit + CI), not just reviewed by hand, across three layers:
| Layer | Tool | Catches |
|---|---|---|
| Static lint | eslint-plugin-jsx-a11y (strict) | Markup-level issues: alt text, label/control association, ARIA misuse, missing keyboard handlers |
| Rendered components | Vitest + Testing Library + axe-core (WCAG 2.2 AA tags) | Violations only visible once a component is rendered to the DOM |
| End-to-end | pa11y-ci (htmlcs WCAG2AA + axe) in a real browser | Color contrast, scrollable regions, and other render-time criteria jsdom can't compute |
The first two layers run per commit. The third needs a built SPA, a running
backend and a real browser, so it is enforced in CI rather than in a hook.
Run it locally with scripts/a11y-pa11y.sh when you touch layout, scroll
containers or colours: it builds the SPA and the server itself and drives
Chromium over every audited page against a scratch database on a free port,
so it never disturbs a running dev stack.
Modal dialogs use the native <dialog> element with showModal(), so focus
trapping, Escape-to-close, top-layer rendering and the ::backdrop come from the
platform rather than hand-rolled code. Details and local commands
(npm run test:run, npm run pa11y) live in frontend/README.md.
CLA in CLA.md. CI runs:
cargo fmt, cargo clippy --all-targets (warnings treated as errors), cargo build (all with SQLX_OFFLINE=true).eslint --max-warnings 0 (incl. jsx-a11y strict), tsc -b, tsc -p tsconfig.test.json, vitest run (axe-core component checks), vite build, plus a pa11y-ci job that audits the built app in a real browser. See Accessibility.php -l + phpcs against moodlehq/moodle-cs.apache2ctl configtest for apache/minerva-app.conf.migrations-immutable blocks edits to already-committed backend/migrations/*.sql files (sqlx content-hashes them at startup).Pre-commit mirrors the same set except the pa11y browser pass, which is too slow for a hook; install with pre-commit install (the hook is wired via pipx install pre-commit).
After editing any sqlx::query! / query_as! macro:
docker compose up -d postgres
cd backend && DATABASE_URL=postgres://minerva:minerva@localhost:5432/minerva \
cargo sqlx prepare --workspace
git add .sqlx/
The committed backend/.sqlx/ cache is what CI and the prod Dockerfile build against; forgetting this step fails locally in the pre-commit cargo check/clippy gate.
AGPL-3.0. Logo by Tilly Makrof-Johansson.
504 commits
35 commits
Rust
63.0%
TypeScript
29.6%
PHP
3.6%
Python
1.2%
RAG platform for educational use at DSV, Stockholm University. Rust/Axum backend, React frontend, Qdrant vector search.
See the codeRAG platform for educational use at DSV, Stockholm University. Teachers upload course materials; students get an AI assistant grounded in those documents, with safeguards designed to support learning.

simple (one-shot retrieve-then-answer) and FLARE (multi-turn, logprob-triggered mid-stream retrieval). The legacy parallel strategy was retired in favour of the tool-use axis below.tool_use_enabled toggle on each course. When on, generation splits into a hidden-thinking research phase (model calls keyword_search / RAG seed / KG-expansion tools, with FLARE's logprob signal injected as a tool event) followed by a clean writeup phase. Per-tool expandable results, research thinking, and a research/writeup token-split are persisted alongside the message and rendered above the assistant bubble.[n] badges (accepts naked-digit and filename-form variants); a right-rail sources panel shows what was actually cited, with a count button that resets the uncited-override on fresh-open.gpt-oss-120b and cross-linked with part_of_unit / solution_of / prerequisite_of / applied_in edges. Retrieval expands top-k along the graph. Gated by the course_kg feature flag.gpt-oss-120b, JSON-schema-strict, reasoning_effort: low) that returns 0..=2 tagged suggestions for the draft the student is typing, severity-coloured against an 8-kind CLEAR-grounded rubric, with a Beginner/Expert calibration toggle. Soft-blocks Send when suggestions are present; Use ideas rewrites the draft via gpt-oss-120b. Mobile drawer + tablet support; per-iteration history persisted for export. Gated by the aegis feature flag.gpt-oss-120b, low effort) + Socratic rewriter (gpt-oss-120b), with KG-driven multi-turn proximity tracking. Gated by the extraction_guard feature flag.Teacher section beside Admin showing the teacher's own spend against their daily cap: today, a rolling window, per course, per provider and per day, including the ingest / classification spend that has no student behind it. When the cap is too low it drafts the increase request (identity, the courses it covers, current limit, busiest day) for lambda@dsv.su.se. The onboarding guide is its second tab.mod_lua; attribute-based role auto-promotion rules.ext: users, in-app data-handling ack, English + Swedish, WCAG 2.1 AA fixes.Detail figures for the document-ingest and chat/RAG pipelines (including the FLARE multi-turn loop): docs/ARCHITECTURE.md.
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() |
Regenerate with docs/screenshots/regenerate.mjs (see docs/screenshots/README.md).
| Layer | Technology |
|---|---|
| Backend | Rust (Axum, SQLx, Tokio) |
| Frontend | React 19, TypeScript 6, Vite, TanStack Router/Query, Tailwind 4, react-force-graph-2d, i18next 26 |
| Frontend runtime | Node 26 (Alpine) in Docker |
| Database | PostgreSQL 16 |
| Vector DB | Qdrant (per-course versioned collections) |
| LLM | Admin-managed chat_models catalog (per-model provider + USD rates). Providers register from env keys: Cerebras (default; gpt-oss-120b across the stack ; classifiers, Aegis, rewrites, writeup), OpenAI, Anthropic, Groq, Gemini, or any OpenAI-compatible endpoint via MINERVA_LLM_BASE_URL__<PROVIDER> |
| Embeddings | OpenAI or local fastembed (memory-budgeted LRU cache, HuggingFace cache persisted on /data0 in prod) |
| Edge | Apache 2 with mod_shib + mod_lua |
cp .env.example .env # add CEREBRAS_API_KEY, OPENAI_API_KEY
docker compose up
Backend on :3000, frontend dev on :5173. With MINERVA_DEV_MODE=true (compose default) Shibboleth is bypassed; the backend reads X-Dev-User and falls back to the first admin in MINERVA_ADMINS.
Production:
docker compose -f docker-compose.prod.yml up -d
# or
docker pull ghcr.io/edwinexd/minerva:master
For the k3s production layout used at DSV, see k8s/.
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string |
QDRANT_URL | Qdrant gRPC endpoint |
MINERVA_HMAC_SECRET | Signs embed/invite/LTI tokens; mirrored to Apache for mod_lua |
MINERVA_ADMINS | Comma-separated admin eppn prefixes |
MINERVA_DOCS_PATH | Document storage path |
CEREBRAS_API_KEY | Inference key; required (the default chat + utility models are Cerebras-hosted) |
OPENAI_API_KEY | OpenAI chat models and OpenAI embeddings (optional with fastembed) |
ANTHROPIC_API_KEY, GROQ_API_KEY, GEMINI_API_KEY | Optional; a provider is registered only when its key is set |
MINERVA_LLM_BASE_URL__<PROVIDER> | Overrides a provider's base URL (self-hosted or proxied endpoints) |
MINERVA_EMBEDDER_URL, MINERVA_RERANKER_URL | gRPC endpoints of the model servers |
MINERVA_BASE_URL | Public base URL for LTI tool URLs |
MINERVA_LTI_KEY_SEED | RSA seed for LTI 1.3 (falls back to HMAC secret) |
MINERVA_SERVICE_API_KEY | Bearer for /api/service/* pipelines |
MINERVA_DEV_MODE | true bypasses Shibboleth |
MINERVA_DEFAULT_COURSE_DAILY_USD | Per-student-per-course spend default (0 = unlimited) |
MINERVA_DEFAULT_OWNER_DAILY_USD | Per-owner aggregate spend default (0 = unlimited) |
MINERVA_CANVAS_AUTO_SYNC_INTERVAL_HOURS | Canvas re-sync interval |
See .env.example for the rest.
| Path prefix | Auth | Why |
|---|---|---|
/api/integration/* | Per-course API key | Moodle server-to-server |
/api/service/* | Global service API key | Automated pipelines |
/api/embed/*, /embed/* | HMAC-signed embed token | Iframe chat |
/lti/* | LTI 1.3 (OIDC + JWT) | LMS-driven login |
/api/external-auth/* | HMAC-signed invite token | External-auth callback |
/embedding-catalog | Public read-only | Teacher feed of enabled models |
| everything else | Shibboleth | Default |
See apache/README.md for the vhost.
Per DSV-IT policy, new sites must meet the accessibility law; Minerva targets WCAG 2.2 level AA. Compliance is enforced automatically (pre-commit + CI), not just reviewed by hand, across three layers:
| Layer | Tool | Catches |
|---|---|---|
| Static lint | eslint-plugin-jsx-a11y (strict) | Markup-level issues: alt text, label/control association, ARIA misuse, missing keyboard handlers |
| Rendered components | Vitest + Testing Library + axe-core (WCAG 2.2 AA tags) | Violations only visible once a component is rendered to the DOM |
| End-to-end | pa11y-ci (htmlcs WCAG2AA + axe) in a real browser | Color contrast, scrollable regions, and other render-time criteria jsdom can't compute |
The first two layers run per commit. The third needs a built SPA, a running
backend and a real browser, so it is enforced in CI rather than in a hook.
Run it locally with scripts/a11y-pa11y.sh when you touch layout, scroll
containers or colours: it builds the SPA and the server itself and drives
Chromium over every audited page against a scratch database on a free port,
so it never disturbs a running dev stack.
Modal dialogs use the native <dialog> element with showModal(), so focus
trapping, Escape-to-close, top-layer rendering and the ::backdrop come from the
platform rather than hand-rolled code. Details and local commands
(npm run test:run, npm run pa11y) live in frontend/README.md.
CLA in CLA.md. CI runs:
cargo fmt, cargo clippy --all-targets (warnings treated as errors), cargo build (all with SQLX_OFFLINE=true).eslint --max-warnings 0 (incl. jsx-a11y strict), tsc -b, tsc -p tsconfig.test.json, vitest run (axe-core component checks), vite build, plus a pa11y-ci job that audits the built app in a real browser. See Accessibility.php -l + phpcs against moodlehq/moodle-cs.apache2ctl configtest for apache/minerva-app.conf.migrations-immutable blocks edits to already-committed backend/migrations/*.sql files (sqlx content-hashes them at startup).Pre-commit mirrors the same set except the pa11y browser pass, which is too slow for a hook; install with pre-commit install (the hook is wired via pipx install pre-commit).
After editing any sqlx::query! / query_as! macro:
docker compose up -d postgres
cd backend && DATABASE_URL=postgres://minerva:minerva@localhost:5432/minerva \
cargo sqlx prepare --workspace
git add .sqlx/
The committed backend/.sqlx/ cache is what CI and the prod Dockerfile build against; forgetting this step fails locally in the pre-commit cargo check/clippy gate.
AGPL-3.0. Logo by Tilly Makrof-Johansson.
504 commits
35 commits
Rust
63.0%
TypeScript
29.6%
PHP
3.6%
Python
1.2%