Aarhus Kommune aktindsigt case management — Flask + HTMX, SQLAlchemy on MSSQL, behind IIS.
The Flask web tier is thin. All heavy or long-running work — OS2Forms intake polling, IMAP polling of the shared mailbox, GO/Nova fetches, screening, retention deletion, warning emails — runs on OpenOrchestrator robots on separate machines. KontAKT enqueues OO work by POSTing to PyOrchestrator API (/api/queue, /api/trigger). OO writes results back to KontAKT by writing directly to its MSSQL tables (or, in the rare case it makes sense, calling a small KontAKT endpoint).
On the MSSQL server (the existing one OO uses), as sysadmin:
CREATE DATABASE KontAKT COLLATE Danish_Norwegian_CI_AS;
ALTER DATABASE KontAKT SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE KontAKT SET ALLOW_SNAPSHOT_ISOLATION ON;Then in the new KontAKT database, run db/schema.sql.
Edit db/create_login.sql and replace the placeholder password with a real strong one. Run it as sysadmin. This creates the kontakt_app login and grants db_datareader + db_datawriter (no DDL — schema changes go through schema.sql).
Two layers:
-
config.tomlat the project root — committed, non-secret. Server names, SMTP host, SMTP from-address, allowed email domain, TTLs, etc. Edit directly. Createconfig.local.tomlfor per-developer overrides (gitignored, merges on top). -
Environment variables — secrets only:
Variable What KONTAKT_SECRET_KEYFlask session signing key. 32+ random chars. KONTAKT_DB_USERNAMEMSSQL login (= kontakt_app).KONTAKT_DB_PASSWORDThe password you set in create_login.sql.KONTAKT_PYORCH_API_KEYX-API-Keyfor the PyOrchestrator API.KONTAKT_GO_USERNAMENTLM user for GO API (case-search autocomplete on step 3). KONTAKT_GO_PASSWORDPassword for KONTAKT_GO_USERNAME.KONTAKT_NOVA_CLIENT_SECRETOAuth client secret for KMD Nova (case-search autocomplete). KONTAKT_OO_API_KEYX-API-Keythat OO uses when calling INTO KontAKT (attachment upload callbacks).
setx KONTAKT_SECRET_KEY "<paste 32+ random chars>"
setx KONTAKT_DB_USERNAME "kontakt_app"
setx KONTAKT_DB_PASSWORD "<the password from step 2>"
setx KONTAKT_PYORCH_API_KEY "<pyorch api key>"
setx KONTAKT_OO_API_KEY "<random 32+ chars — shared with OO scripts>"setx persists into your user environment — open a new terminal to pick up the values. To generate a secret key:
python -c "import secrets; print(secrets.token_urlsafe(48))"Use iis/web.config as a template. The <environmentVariables> block under <httpPlatform> is where the four secrets go. Everything else stays in config.toml.
Python 3.11+ required (for stdlib tomllib).
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -e .[dev]Self-contained for behind-firewall deployment — vendor the file:
New-Item -ItemType Directory -Force app\static\vendor | Out-Null
Invoke-WebRequest -Uri "https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js" -OutFile "app\static\vendor\htmx.min.js"$env:FLASK_APP = "wsgi.py"
python -m flask run --debugVisit http://localhost:5000 → /auth/login. With auth_dev_mode = true in config.toml, the magic link prints to the console — no SMTP needed for local dev.
wsgi.py is the entry point. The recommended pattern is HttpPlatformHandler (modern replacement for FastCGI for Python on IIS):
- Install HttpPlatformHandler on the IIS server.
- Drop
iis/web.configinto the site root next to the project; replace theREPLACE-MEvalues. - The app pool identity needs read+execute on the project folder and read+write on
iis/logs/(created at runtime).
HttpPlatformHandler launches waitress as a child process with the env vars from <environmentVariables> set, and proxies HTTP to it on a private port.
- Magic link to
*@aarhus.dk, raw token in email, only SHA-256 hash in DB, 15-min TTL, one-time use. - Users are not auto-created. An admin must add the user row first; otherwise the magic link, while still sent, will fail to verify.
- Self-service "request access" page is planned — sends the email to admin queue for approval.
- Every authentication-related action (
magic_link_requested,user_login,user_login_failed,user_logout) is recorded inaudit_logwith IP and email context.
Auth is isolated in app/auth/service.py so it can be swapped for OIDC/Entra ID later without touching routes.
KontAKT/
├── config.toml # non-secret config, committed
├── db/
│ ├── schema.sql # canonical schema — run once on fresh DB
│ └── create_login.sql # creates the kontakt_app MSSQL login
├── iis/
│ └── web.config # IIS HttpPlatformHandler template
├── app/
│ ├── __init__.py # app factory
│ ├── config.py # TOML + env loader
│ ├── extensions.py # db = SQLAlchemy()
│ ├── models.py
│ ├── audit.py # audit.log() helper
│ ├── auth/ # magic-link login
│ ├── main/ # dashboard + case views (stub)
│ ├── templates/
│ └── static/
├── wsgi.py
└── pyproject.toml