All documentation Download (Markdown) Technical

Contract · contract.pdhc.se

Technical manual


Contract Service — Architecture

Technical architecture of the PDHC Contract Manager, covering container topology, data flows, data model, and security posture.


1) System overview

1.1 Purpose

The Contract Manager is a FHIR R5 microservice for creating, reading, updating, and deleting healthcare contract resources. It provides public read access with rate limiting and admin-authenticated write access via JWT tokens.

1.2 Position in the PDHC platform

The Contract Manager is one service in the PDHC family, alongside:

Each service runs independently on its own port range and Docker Compose project.

The platform has three peer concepts for governing who may see
whose data. They are not interchangeable. New code consistently
gets this wrong — the symptom is usually a contract being asked to
do something it cannot, and a Patient* row not being created where
one should have been. Pick by what is being expressed:

You want to express… Use… Lives in PDL/legal basis
"Organisation A may submit observations on concepts C[] under provider B's care plans" Contract (term[] with request_scope / return_scope) contract.pdhc Civil agreement between two orgs — not a patient-data ruling
"Patient P consents that caregiver G may read their data (optionally only concepts C[])" PatientConsent ips.pdhc (/api/v1/patients/<guid>/consents) Lag (2022:913) § 5 cohesive-care consent
"Patient P blocks caregiver/clinic S from reading their data" PatientBlock ips.pdhc (/api/v1/patients/<guid>/blocks) PDL Ch 4 § 4 spärr

The shape is the giveaway:

When a contract is signed and a Patient/<guid> reference appears in
signer[], contract.pdhc emits a PatientConsent row on ips.pdhc as
a side effect (granted_via='contract', contract_guid=<linkback>).
The signer reference and the auto-emitted consent are two distinct
artefacts in two distinct services, related by contract_guid:

If you only need patient consent (no civil agreement, no concept
scope, no provider org party) — author the PatientConsent directly
on ips.pdhc. Inventing a contract just to get a consent row is the
wrong tool.

If you only need a block — go straight to PatientBlock. A contract
cannot revoke another organisation's read rights to a patient's data;
that is structurally a different decision (the patient's, not the
caregiver's).


2) Container topology

2.1 Architecture diagram

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   browser    │────▶│  web:9022   │     │  db:9020    │
│              │     │  (nginx)    │     │ (PostgreSQL) │
└─────────────┘     └─────────────┘     └──────┬──────┘
                           │                    │
                           │ API calls          │ SQL
                           ▼                    │
                    ┌─────────────┐             │
                    │  api:9021   │─────────────┘
                    │  (Flask)    │
                    └─────────────┘

2.2 Port map

Service Container Port Host Port Purpose
db 5432 9020 PostgreSQL database
api 9021 9021 Flask REST API
web 80 9022 nginx serving SPA + docs

All ports are within the 9020–9030 range as required by project rules.

2.3 Service-to-service consumers

The Contract Manager exposes an internal API consumed by other PDHC services:

This internal layer is separate from the public FHIR API and admin JWT endpoints.


3) Data flows

3.1 Public read flow

Browser → GET localhost:9022 → nginx serves index.html (SPA)
SPA JS → GET localhost:9021/fhir/Contract → Flask → PostgreSQL → JSON response

Rate limiting is applied per IP at the Flask layer (flask-limiter, in-memory store).

3.2 Admin write flow

SPA JS → POST localhost:9021/auth/login → JWT token returned
SPA JS → POST localhost:9021/fhir/Contract (Bearer token) → Flask validates JWT + role → PostgreSQL INSERT → 201

3.3 Auth flow

  1. Client sends POST /auth/login with username + password
  2. Flask verifies credentials against the users table (bcrypt hash)
  3. On success, Flask issues a JWT with claims: identity (user guid), role, username
  4. Token expires after 8 hours
  5. Client includes Authorization: Bearer <token> on admin requests
  6. Flask-JWT-Extended validates the token and extracts claims

4) Data model

4.1 Users table

Column Type Constraints
guid VARCHAR(36) Primary key, UUID v4
username VARCHAR(128) Unique, not null
password_hash VARCHAR(255) Not null (bcrypt)
role VARCHAR(16) Not null ("admin" or "reader")
is_active BOOLEAN Not null, default true
created_at TIMESTAMPTZ Not null, auto-set to UTC now

4.2 Contract records table

Column Type Constraints
guid VARCHAR(36) Primary key, UUID v4
fhir_contract JSON Not null, stores the full FHIR R5 Contract resource
created_at TIMESTAMPTZ Not null, auto-set to UTC now
updated_at TIMESTAMPTZ Not null, auto-updated on modification

The fhir_contract JSON includes the full FHIR R5 Contract shape plus four PDHC-defined extensions (kept inside Contract.extension[] so the JSON stays portable across FHIR servers — they ride along with the contract record without needing a platform-specific column):

Extension URL Type Purpose
https://contract.pdhc.se/StructureDefinition/legally-ok bool Operator has signed off on legal terms
https://contract.pdhc.se/StructureDefinition/pub-exists bool A personuppgiftsbiträdesavtal (data-processor agreement) exists
https://contract.pdhc.se/StructureDefinition/legal-provider bool Provider is a legally registered entity
https://contract.pdhc.se/StructureDefinition/provider-data-status code ok / deficient / unclear — provider-data verification verdict

The status field of Contract.status is constrained at the UI layer to four FHIR R5 codes — negotiable (Under consideration), executed (Active — only this state qualifies the contract as a basis for fulfilling requests), terminated (Expired), revoked (Revoked). Other FHIR codes are accepted via direct API for compatibility with externally authored Contracts.

4.3 GUID rules


5) Security posture

5.1 Authentication

5.2 Authorization

5.3 Rate limiting

5.4 CORS

5.5 Database security