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.