Technical manual
1177.pdhc is the platform's patient-facing form host: it ingests a FHIR
questionnaire targeted at a patient (pushed by request.pdhc), renders it in
a 1177-style web UI, collects the answers, produces a FHIR R5
QuestionnaireResponse, and dispatches it to gateway.pdhc. Internal DB
name forms_1177; branding "1177 Forms".
pdhc_keyauth module for127.0.0.1, behind nginx TLS at 1177.pdhc.se),forms_1177_db). App runs bare-metal under gunicornplan.pdhc (Questionnaire) → request.pdhc (ServiceRequest Bundle)
│ POST /api/webhook/inbound (X-API-Key)
▼
Assignment (one per Questionnaire, status=pending)
│ operator/patient fills + submits (UI or API)
▼
QuestionnaireResponse (FHIR R5, stored) ──► gateway.pdhc
POST {GATEWAY_URL}/api/v1/provider/report/<request_guid>
headers: X-Provider-Token; body {patient_guid, grant_token,
status:"completed", report_payload:<QuestionnaireResponse>}
Health (routes/health.py) — GET /api/health, no auth; runs SELECT 1,
returns {status, database}; CORS Access-Control-Allow-Origin:
https://www.pdhc.se (so services.html can read it).
Webhook (routes/webhook.py, @require_api_key) —
POST /api/webhook/inbound: parse the FHIR Bundle, read delivery meta.tag
(system https://pdhc.se/delivery: grant_token, contract_guid,
organisation_guid, expires_at), unwrap the ServiceRequest + contained
Patient + each Questionnaire, strip the questionnaire- id prefix, and
create one Assignment per Questionnaire (pending). 201 with the created
assignments; 400 if no Questionnaire is inlined (request.pdhc must resolve it
from plan.pdhc first).
Assignments REST (routes/assignments.py, @require_api_key):
| Method | Path | Notes |
|---|---|---|
| GET | /api/assignments?patient_guid=&status=&limit=&offset= |
paginated summaries (patient_guid required) |
| GET | /api/assignments/<guid> |
full assignment |
| POST | /api/assignments/<guid>/submit |
body {items:[{linkId,text,answer:[…]}]} → builds QR, dispatches; 409 if already completed/cancelled/expired |
| POST | /api/assignments/<guid>/cancel |
pending/opened → cancelled |
Admin web UI (routes/admin.py, @login_required): /dashboard,
/assignments (paginated, archive-aware), /assignments/<guid> (renders the
form; auto pending→opened; read-only once terminal), /assignments/<guid>/submit,
/assignments/<guid>/archive, /settings, /settings/api-keys*,
/settings/cleanup* (bulk delete guarded by confirm=DELETE). Login/keys via
pdhc_keyauth (/login, /logout, /keyauth/*).
Not live: routes/inbound.py (/inbound/push) exists but its blueprint is
not registered — dead code, ignore.
app/models/)| Table | Purpose | Key columns |
|---|---|---|
assignments |
one questionnaire assigned to a patient | assignment_guid, patient_guid, form_guid, form_version, questionnaire_fhir (JSON, verbatim), request_guid, grant_token, contract_guid, organisation_guid, grant_expires_at, status (pending|opened|completed|expired|cancelled), response_guid, archived |
questionnaire_responses |
submitted answers | response_guid, form_guid, form_version, patient_guid, assignment_guid, fhir_json (JSON QR), submitted_at |
api_keys |
service/webhook keys (sha256, scoped, revocable) | key_hash, name, scope, is_active, expires_at |
Plus the shared pdhc_keyauth tables: keyauth_users, keyauth_keys,
keyauth_access_log (patient-data access audit). Schema via
app/scripts/init_db.py (db.create_all()); ad-hoc migrations:
flask migrate-assignments, flask migrate-archived.
Bundle → ServiceRequest with contained Patient +Questionnaire (R5). Item extensions parsed for rendering:questionnaire-unit, minValue/maxValue,questionnaire-sliderStepValue, questionnaire-itemControl (slider),code[] (LOINC/SNOMED), repeats.QuestionnaireResponse: questionnaire=form_guid,status=completed, subject/author = Patient/<patient_guid>, authored,item[], and basedOn=[ServiceRequest/<request_guid>] when present. AnswervalueCoding, string/text→valueString,valueInteger, decimal→valueDecimal, boolean→valueBoolean,valueDate, dateTime→valueDateTime; empty optional items omitted.id (minus the questionnaire- prefix) is theform_guid, tracing back to the plan.pdhc form.pdhc_keyauth moduleBOOTSTRAP_ADMIN_USER /BOOTSTRAP_ADMIN_PASSWORD.X-API-Key, checked against theAPI_KEY then hashed api_keys records (scoped, revocable).X-Provider-Token (PAT) + the carriedgrant_token. Org/contract are no longer sent on the report; the gatewaygrant_expires_at; expired grants rejected downstream, statusexpired..env (see .env.example — but note the app reads GATEWAY_URL
(default https://gateway.pdhc.se) and GATEWAY_PROVIDER_TOKEN, whereas
the example file still lists stale GATEKEEPER_* names):
DATABASE_URL=postgresql+psycopg://forms_user:…@localhost:9037/forms_1177
API_KEY=<service webhook key>
SECRET_KEY / ALLOWED_ORIGINS
GATEWAY_URL=https://gateway.pdhc.se GATEWAY_PROVIDER_TOKEN=<PAT>
BOOTSTRAP_ADMIN_USER / BOOTSTRAP_ADMIN_PASSWORD
Deploy: docker-compose.yml runs Postgres 16 only (forms_1177_db, volume
forms_1177_data, 127.0.0.1:9037). App runs under gunicorn via start.sh /
safe_restart.sh (bind 127.0.0.1:9036, 2 workers, timeout 30, daemon, PID
file, OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES for macOS). safe_restart.sh
pg_dumpalls to db_backups/ (keeps last 10) before restarting and runs a
5-attempt /api/health check. nginx (server_configs/…_ssl) terminates TLS
on 1177.pdhc.se and proxies to 127.0.0.1:9036.
tests/test_{webhook,routes,models,assignment_form}.py (~52 passing) —
webhook bundle parsing, REST + admin routes, models, and the type-aware form
rendering/submission.