Configuration & deployment¶
The backend is a plain Python (FastAPI) app. The web apps (wizard + console) are
pre-built into app/static/, so the container needs no Node step.
Run locally¶
python3.12 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
SEED_DEMO=1 python run.py # http://localhost:8000
/dashboard— admin console/wizard/— registration wizard/docs— interactive API docs
Demo logins (only when SEED_DEMO=1): admin@demo.com / admin123 and
user@demo.com / user123.
Rebuild the web apps after changing them:
cd wizard && npm install && npm run build && cd ..
cd admin && npm install && npm run build && cd ..
Configuration¶
Set as environment variables, or in a provider_config.json at the repo root
(env wins). Everything has a working default.
| Key | Meaning |
|---|---|
DIRECTORY_MODE |
search (public catalog), curated (one organization) or portal (many provider organizations) — see Three ways to run it |
PROVIDER_NAME / PROVIDER_TAGLINE |
Branding on the app landing + console |
REGISTRY_PUBLIC_URL |
Canonical https:// origin. Required in portal mode |
AUTO_APPROVE |
New submissions go live immediately (default true) |
WEBHOOK_URL / WEBHOOK_SECRET |
POST on each new complaint (Slack/Discord/…) |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORD / SMTP_FROM |
Send owner emails; without these the console opens a pre-filled mail draft instead |
Production must-sets¶
Set these before a real production deploy
SECRET_KEY— a strong random string. Without it, sign-in tokens use a throwaway key that resets on every restart.ADMIN_EMAIL+ADMIN_PASSWORD— on a fresh database the first admin is created from these. LeaveSEED_DEMOunset so demo credentials are never shipped.DATABASE_URL— optional; point at PostgreSQL for many concurrent writers. Defaults to SQLite (with WAL enabled), which is fine for the pilot.
Deploy (Docker + Traefik)¶
docker-compose.prod.yml attaches the container to a Traefik web network with
TLS. Persist ./data as a volume. Verify after deploy:
Running a provider portal¶
A portal is the same image with a different mode, its own database and its own hostname. Run it alongside an existing catalog rather than converting one.
DIRECTORY_MODE=portal
REGISTRY_PUBLIC_URL=https://providers.example.com
SECRET_KEY=<a long random string>
ADMIN_EMAIL=you@example.com
ADMIN_PASSWORD=<set before first boot>
REGISTRY_PUBLIC_URL is not optional here
Behind a reverse proxy the request the app sees is plain HTTP on an internal hostname. The address a provider is handed ends up compiled into a shipped mobile binary — set this, or every provider ships the wrong URL.
Start from an empty database
A row created before the switch belongs to no organization, so it appears in
no /p/<slug> directory. That is the safe outcome, not a useful one. Give a
portal its own database and its own ./data volume.
The repository ships docker-compose.portal.yml as a second Traefik service on
the same host — its own Compose project name, container name, volume
(./data-portal), Host() rule and .env.portal:
cp .env.portal.example .env.portal # then fill it in
mkdir -p data-portal
docker compose -f docker-compose.yml -f docker-compose.portal.yml up -d --build
The project name is what keeps the two apart
Both stacks are launched from the same directory, and Compose identifies a
project by that directory. docker-compose.portal.yml sets
name: openedx-lms-portal for exactly this reason — without it, starting the
portal reads as recreate the public catalog with new settings and takes the
running pilot down. Don't remove it, and check docker compose … config | head -1
says name: openedx-lms-portal before you bring anything up.
Point a DNS A record at the server for the Host() in the compose file first;
Traefik gets the certificate on the first request.
Verify:
curl -s https://providers.example.com/api/v1/config # {"directory_mode":"portal",…}
curl -s https://providers.example.com/p/<slug>/api/v1/config # {"directory_mode":"curated",…}
Tests¶
Covers cross-organization isolation in portal mode, that search and curated
still behave exactly as before, and that the schema migration is safe to run twice
against a copy of the production database.