Skip to content

Configuration

Backend settings are read from backend/.env by pydantic-settings. A handful of site switches live in Redis instead and take effect without a restart.

Never commit a value

Everything below is a name. The values are in backend/.env, frontend/.env and ./secrets, none of which belong in a document, a screenshot or a paste.

Core

Variable Default Notes
DATABASE_URL sqlite:///./quiz.db The real one points at Postgres.
SECRET_KEY a placeholder Signs JWTs and the OIDC state cookie. Change it in production.
REDIS_URL redis://localhost:6379/0 Must name quiz-redis-1 — see the alias note.
APP_URL The public origin. Email links and the OIDC redirect are built from it, and it decides whether the session cookie is https_only.
ALGORITHM HS256
ACCESS_TOKEN_EXPIRE_MINUTES 1440 A day.
LOG_LEVEL INFO

Rate limits

Variable Default
LOGIN_MAX_ATTEMPTS 10
LOGIN_WINDOW_MINUTES 15
REFRESH_MAX_PER_HOUR 600

The login pair is vestigial — there is no password door. REFRESH_MAX_PER_HOUR is generous on purpose: one address can be a whole hospital behind one NAT, and every app launch refreshes.

The per-user limits are not environment variables:

Limit Value Key
Tutor 30 a day teach_chat_daily:{user_id}:{date}
AI Mode 60 a day ai_mode_daily:{user_id}:{date}
Text to speech 240 an hour tts_speak:{user_id}
Transcription 120 an hour stt_transcribe:{user_id}

Admins, moderators and users marked unthrottled are exempt. Rate limiting degrades open — if Redis is unavailable, requests are allowed through.

AI

Variable Default Notes
LITELLM_API_BASE empty The proxy. Empty means direct OpenAI.
LITELLM_API_KEY empty
LITELLM_MODEL gpt-4o-mini The last-resort fallback when no model is configured for a task.
LITELLM_EMBEDDING_MODEL empty Environment only — see below.
LITELLM_RERANK_MODEL cohere-rerank-v4.0-pro Overridable live at settings:rerank_model.
EMBEDDING_DIMENSIONS 1024
AI_REASONING_EFFORT none
AI_MODE_ENGINE agent agent uses the structured tutor schema; anything else uses the plain prompt path.

The embedding model is a deployment, not a setting

PUT /admin/settings refuses embedding_model with a 400. Every vector in the database was produced by the current model; changing it makes all of them incomparable and search returns noise until every question, article and card has been re-embedded. A change that expensive belongs in the environment, where making it is a deployment somebody decided on.

The README still says this can be changed live from the admin page. It cannot.

Storage

Variable Default Notes
STORAGE_BACKEND local local or s3. Reads fall back to the volume either way, so files migrate gradually.
UPLOAD_DIR ./uploads
S3_ENDPOINT_URL http://minio:9000
S3_ACCESS_KEY, S3_SECRET_KEY empty
S3_BUCKET pedshub-media
S3_REGION us-east-1
MAX_UPLOAD_SIZE 104857600 100 MB. Keep nginx's client_max_body_size just above it.
CHROMA_PERSIST_DIR ./chroma_data

Identity

Variable Notes
OIDC_PROVIDER_URL Discovery URL. Blank disables SSO — and since there is no password door, that means nobody can sign in.
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET
OIDC_SCOPES openid email profile
OIDC_PROVIDER_NAME The name on the button. Default SSO.
OIDC_ROLE_CLAIM groups for most providers, roles for Entra app roles. Blank means no mapping and everybody who signs in is a learner — the safe default.
OIDC_ADMIN_GROUPS, OIDC_MODERATOR_GROUPS Comma-separated, matched case-insensitively. Admin wins over moderator.

Setting OIDC_ROLE_CLAIM plus at least one group list is what makes the two in-app role endpoints answer 409. See Signing in.

Email

MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_FROM, MAIL_STARTTLS, MAIL_SSL_TLS, and ADMIN_EMAIL for where contact-form submissions go.

The clinical library

Variable Notes
CLINICAL_MILVUS_URI, CLINICAL_MILVUS_TOKEN Read-only. Nothing here writes to it.
CLINICAL_MILVUS_COLLECTION mcp_bge_m3_1024
CLINICAL_MCP_URL The MCP transport, used by the ai-draft path

There are two Milvus servers on this host. This is the one holding mcp_bge_m3_1024, reached as milvus. The other, on ped-ai-storage_basic, is a different instance with different credentials.

Vestigial variables

Present in config.py, read by nothing in the application:

Variable Why it is still there
OPENAI_API_KEY, ELEVENLABS_API_KEY, GOOGLE_TTS_API_KEY Every text-to-speech path now goes through the LiteLLM gateway.
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_BEDROCK_REGION
BBB_SERVER_URL, BBB_SECRET BigBlueButton. No code references either.
DEFAULT_ADMIN_EMAIL, DEFAULT_ADMIN_PASSWORD Bootstrap seeding, from before SSO-only.
LOCAL_SPEECH_GATEWAY_URL

CAP_SITE_KEY and CAP_PUBLIC_URL remain in frontend/.env and are read by nothing: the entrypoint now writes an empty window.__APP_CONFIG__ = {}. The file and the script stay because index.html loads /config.js unconditionally, and a missing one is a 404 on every page load.

Settings that live in Redis

Set from Settings → the admin panel, read with a default whenever Redis is unavailable, so the site keeps working when it is not.

Flag Default Is
sharing_enabled true May a learner create a public share link for a session
tutor_in_quiz true May the tutor be opened during a study attempt. It is never offered in an exam.
clinical_library_enabled false May an AI draft be grounded in the clinical library
pubmed_enabled false May an AI draft search PubMed
Value Default Is
clinical_mcp_url empty Where the clinical library answers. Blank means there is not one.
pubmed_api_key empty Read back masked. A settings page that echoes a secret puts it in every screenshot and browser cache from then on.
pubmed_contact_email empty NCBI raises the rate limit for a caller who identifies themselves.

Anything not in these two lists cannot be set. A typo fails loudly rather than writing a key nothing will ever read.

There is deliberately no registration switch: accounts are created at the identity provider, never here.