φ(ai) PHI AI
DocumentationDocs
Repository
On this page — 16 sections

Setup & deployment — bring your own infrastructure

The complete, step-by-step implementation guide for the PHI AI Platform: what you must bring, what the platform provisions, and every step from an empty cloud account to a verified deployment. This page is the primary implementation resource; the repository's runbooks/ directory carries the per-cloud command detail, and each step below names its runbook.

1. What you are deploying

The platform is a set of cooperating components, each of which maps to infrastructure you bring:

  • The application — Python/FastAPI services (web interface, ingestion, bulk scheduler, delivery), shipped as containers and run with Docker Compose on compute you provide.
  • Object storage — three versioned buckets/containers: the record store, the audit store, and a physically separate psychotherapy store. Every object is envelope-encrypted before it lands.
  • A KMS — organization-managed keys that wrap each object's data key. Key material never leaves your key service; key use is logged by your cloud.
  • PostgreSQL — the queryable index (no clinical content, by design), local accounts when enabled, prompt history, platform state, the retrieval index, telemetry, and the OMOP analytics layer.
  • Identity — your reverse proxy / identity provider, or the platform's optional local accounts, or SMART-on-FHIR launch from the EMR.
  • A model provider — your cloud's own model service (AWS Bedrock, GCP Vertex AI, Azure AI Foundry), each of which serves several vendors' frontier models, or a vendor's API called directly, plus any models you bring through the Control panel's registry. The platform is not tied to one model vendor: the assistant reads excerpts and returns text, and every capability it has is defined here rather than by whose model is answering.
  • The ambient listener (optional) — a self-hosted speech-recognition model (faster-whisper, MIT) for ambient documentation, running on your compute so audio never leaves the account; registered and governed through the model registry's ambient slot.
  • An EMR connection — a backend app registered with your vendor (Epic, Oracle Health, athenahealth, eClinicalWorks, MEDITECH, NextGen).

2. Bring your own infrastructure — the matrix

All three clouds have a complete Terraform stack under deploy/aws, deploy/gcp and deploy/azure. Terraform provisions the starred rows; every row can instead be satisfied by infrastructure you already run, provided it meets the stated requirement.

ComponentAWSGCPAzureHard requirements if you bring your own
Record / audit / psychotherapy stores *S3 ×3GCS ×3Blob ×3 Versioning ON; no immutability lock (Object Lock / retention lock is unsupported by design — retention is recorded, not storage-enforced); bucket policy denies non-TLS and TLS < 1.2; separate psychotherapy store with its own access policy.
Key management *KMS CMKCloud KMSKey Vault Organization-managed key(s); the app role holds encrypt/decrypt only; key-use logging enabled.
PostgreSQL *RDSCloud SQLDatabase for PostgreSQL PostgreSQL 14+; network path from the app host; separate least-privilege roles for ingest, reader, analytics (the per-cloud bootstrap SQL creates them); IAM database auth supported where the cloud offers it.
IAM / service identity *IAM rolesService accountsManaged identities Least privilege per component: the web role reads; the ingest role writes store+index; nothing holds key administration. Purpose-of-use rides as a session tag on AWS.
Computeanything running Docker + Docker Compose Outbound TLS to your EMR, storage, KMS, Postgres and model provider; inbound only from your proxy.
Identity / TLS terminationyour proxy or IdP The app binds localhost by default and trusts a proxy-established identity — or enable local accounts, or SMART launch. Never expose the app port directly.
Model providerBedrock, or a vendor APIVertex AI, or a vendor APIAzure AI Foundry, or a vendor API A BAA-covered path for PHI-reading configurations — whichever model you choose. Each cloud's own model service keeps traffic inside the account you already hold a BAA for, and each serves several vendors' frontier models, so the cloud column does not dictate the model. A model called directly over its vendor's own API instead requires a separate agreement with that vendor.
BYO models (any kind)your inference endpoints HTTPS only (the registry rejects non-TLS endpoints); registered → enabled → activated through the audited Control panel lifecycle.

3. Choose your deployment size — before you ingest anything

The platform ships two deployment shapes, selected with a single setting, and the choice is core to the design: a small-to-medium deployment and a large deployment are not the same system with different instance sizes — they use different storage layouts. Set it in .env before first ingest:

  • PHI_AI_PROFILE=small — the default, and correct for small-to-medium deployments (a clinic, a practice group, a single hospital): one encrypted object per resource, per-resource integrity digests, per-resource disposal, and a simple unpartitioned index (core/db/schema.sql). Right up to roughly 200–500 million stored resources.
  • PHI_AI_PROFILE=large — the scalable shape for large deployments (a health system or large implementation requiring terabyte–petabyte scale AI): NDJSON bundles per (patient, resource type) — roughly 500× fewer storage objects — with the index LIST-partitioned on resource type (core/db/schema_partitioned.sql). At 20 billion resources this is the difference between 20 billion objects with a 7.3 TB index and 40 million objects with a 15 GB index.

The number that decides is object count, not terabytes. 100 TB of imaging can be on small; 20 TB of discrete 5 KB FHIR resources belongs on large. Estimate how many resources you will store — and for DICOM, count instances, not studies (the platform stores one object per SOP instance so a viewer can fetch a single slice). Sizing table for the index, one row ≈ 400 bytes:

Stored resourcesIndex sizeDatabaseProfile
1 million~0.4 GBdb.t4g.microsmall
100 million~40 GBdb.m6g.large, 200 GBsmall
500 million~200 GBdb.m6g.xlargethe crossover — decide deliberately
1 billion+~400 GB+sized per docs/SCALING.mdlarge

What large costs, stated plainly: integrity and disposal move from per-resource to per-bundle (disposing one resource rewrites its bundle), and it assumes the source system is retired — against a live, appending EMR, small is correct at any size. The profile is a deployment shape, not a tuning knob: it determines the object keys, so changing it on a populated store migrates nothing — new resources land in the new layout while existing ones stay put. Choose before ingesting.

The infrastructure sizing follows the same split. Terraform ships sized variable files — terraform.tfvars.free-tier.example for a small/dev stack and terraform.tfvars.large-deployment.example (roughly 50 TB and above) — covering the database class, cold-tier lifecycle transitions (only worthwhile when objects are mostly above the 128 KB per-object billing floor, which bundling helps clear), and CloudTrail data-event scope. Operations were built to survive the large shape: audit verification is checkpointed so routine runs read only what is new, and storage↔index reconciliation streams as a merge join so memory tracks discrepancies, not store size. The full treatment — economics, ceilings, and what was rebuilt for scale — is docs/SCALING.md; it is the authoritative companion to this section.

4. Prerequisites — do not start without these

  1. A signed BAA with your cloud provider, active for the target account, before provisioning anything (AWS: through AWS Artifact). Even development deployments should build this habit.
  2. Admin access to create object storage, KMS keys, IAM roles/service accounts, and Postgres in the target account.
  3. Tools: Terraform ≥ 1.10 (native state locking), your cloud's CLI (AWS CLI v2 / gcloud / az), Python 3.11+, Docker + Docker Compose, OpenSSL, psql. Verify identity first (aws sts get-caller-identity or equivalent).
  4. An EMR developer account: Epic at fhir.epic.com (Backend Services app), or your vendor's console — each vendor's registration path is a chapter in docs/EMR_CONNECTORS.md.
  5. A retention decision, confirmed against docs/COMPLIANCE.md for your state and data types. Retention here is recorded and enforced by workflow, not by a storage lock — read that section before accepting the posture.

5. Step 1 — provision the infrastructure

Follow your cloud's setup runbook — it is the authoritative, command-level version of this step: runbooks/RUNBOOK_AWS_SETUP.md, RUNBOOK_GCP_SETUP.md (or RUNBOOK_GCP_ZERO_TO_ONE.md for a from-nothing walkthrough), or RUNBOOK_AZURE_SETUP.md. The shape on every cloud:

  1. Bootstrap the Terraform state backend (deploy/aws/bootstrap creates the state bucket; copy backend.hcl.examplebackend.hcl with your account values).
  2. Configure variables: copy terraform.tfvars.exampleterraform.tfvars. Sized examples match section 3's profiles (.free-tier.example, .large-deployment.example). Development stacks may set require_mfa_to_assume_roles=false; production must not.
  3. terraform init -backend-config=backend.hcl && terraform plan && terraform apply. Review the plan: confirm every bucket denies non-TLS and that no immutability lock is configured.
  4. Capture outputs: terraform output env_fragment emits the storage, KMS, IAM and database lines for your .env.

What this creates: the three versioned stores, organization-managed KMS keys, least-privilege roles (each scoped to only the buckets and keys it needs), optional RDS/Cloud SQL, CloudTrail/data-access logging, and a budget guard.

6. Step 2 — bootstrap the databases

Apply the per-cloud bootstrap SQL (each creates the schema plus the least-privilege roles; all are re-runnable):

ConcernSchemaPer-cloud bootstrap
Record indexcore/db/schema.sql (schema_partitioned.sql under PHI_AI_PROFILE=large — section 3)bootstrap_aws.sql / _gcp / _azure
Local accounts (optional)core/db/users_schema.sqlusers_bootstrap_*.sql
Prompt historycore/db/prompts_schema.sql
Platform state (Control panel config + model registry)core/db/platform_state_schema.sqlapplied lazily by the app
Clinical text retrievalcore/db/retrieval_schema.sqlretrieval_bootstrap_*.sql
Assistant telemetrycore/db/telemetry_schema.sqltelemetry_bootstrap_*.sql
OMOP analyticscore/db/omop_schema.sql + omop_vocab_schema.sqlomop_bootstrap_*.sql · vocabularies per RUNBOOK_OMOP_SETUP.md
Imaging index (optional)core/db/imaging_schema.sqlper RUNBOOK_DICOM_IMAGING.md

7. Step 3 — run the guided installer

python3 install/installer_chatbot.py asks for your cloud, the storage/KMS values from Step 1's outputs, EMR connection details and retention settings, and writes .env (mode 600, never committed). Values already present from terraform output env_fragment are preserved, not discarded. If your source EMR is not Epic, set PHI_AI_EMR_VENDOR to one of epic · cerner · athenahealth · eclinicalworks · meditech · nextgen — it selects the vendor capability profile and is validated at startup, so a typo refuses to start next to its cause.

8. Step 4 — register with your EMR

  1. Epic (the reference path): register a Backend Services app at fhir.epic.com; generate the RSA keypair with scripts/generate_epic_keypair.sh; host the public half at a JWK Set URL (deploy/aws/README_EPIC_JWKS.md shows an S3-hosted pattern). There is no client secret — auth is a signed JWT client assertion (RFC 7523). Provision the Group FHIR ID for bulk export. Each health system must mark the app ready for production and sign Epic's API subscription agreement; write APIs are enabled per-resource and per-flavor, so enumerate each one.
  2. Oracle Health / eClinicalWorks / MEDITECH / NextGen: same keypair model through their own portals; Oracle requires each resource scope granted explicitly (verify conformance per-resource). NextGen has no bulk export — population ingestion falls back to per-patient reads, and the platform's managers refuse bulk operations against it by design.
  3. athenahealth: OAuth2 client credentials — the one vendor that issues a client secret; store it in .env only.
  4. Verify before first ingest: the conformance probe (core/fhir/conformance_probe.py, surfaced on the EMR conformance screen) checks what the vendor actually exposes against what the profile assumes.

9. Step 5 — install and start

./install/install.sh builds and starts the containers. It starts only app and scheduler when PHI_AI_FHIR_GROUP_ID is unset — deliberately, so a deployment without a bulk group does not restart-loop the bulk scheduler.

10. Step 6 — the web interface and identity

Governance note: this system is not the source of truth for users or roles. It is designed to be governed by your organization's identity source of truth — accounts and role assignments live in your IdP / directory, and the platform enforces the identity and roles that source asserts. Plan the integration below accordingly; local accounts are the fallback for organizations without an IdP, never the preferred design.

  1. The web app (python -m core.web inside the app container) binds localhost by default: in the recommended deployment it does not authenticate — it trusts the identity your reverse proxy establishes (PHI_AI_WEB_TRUST_PROXY_AUTH). Put your SSO in front; never expose the port directly.
  2. Or enable local accounts (PHI_AI_WEB_LOCAL_ACCOUNTS + PHI_AI_WEB_SESSION_SECRET + the users bootstrap SQL; runbooks/RUNBOOK_LOCAL_USERS.md) for organizations without an IdP — the only mode in which the platform stores a credential.
  3. Or SMART-on-FHIR launch from inside the EMR (runbooks/RUNBOOK_SMART_LAUNCH.md; issuer allowlist file + PHI_AI_WEB_SMART_REDIRECT_URI).
  4. PHI_AI_WEB_SESSION_SECRET must be set and stable for any real deployment — an ephemeral secret signs everyone out on restart and breaks load-balanced replicas.
  5. Development evaluation only: PHI_AI_WEB_DEV_PERSONAS provides the role-switching personas; it fabricates identity and must never reach production.
  6. Roles: nine, including sysadmin (the only wildcard). The same roles exist as IAM roles so the two layers cannot disagree. Mapping your EMR and directory populations onto them is the next section — do not skip it, because until a person carries a role they can sign in and reach nothing.

11. Step 6b — map your EMR users and roles to permissions

This is the step that turns "SSO works" into "the right people see the right things." It has exactly three links in the chain, and you configure only the first:

your directory group  →  a PHI AI role  →  enumerated permissions  →  enforcement
(or EMR population)      (9, fixed)        (fixed, in software)       route · tool · option
  ^ you map this         ^ you assign      ^ not editable             ^ not bypassable

Roles and their permission sets are defined in the software (core/web/auth.py) and are deliberately not configurable: a screen can never be reached by a role that was quietly granted something extra in a config file. What you own is which of your people land in which role.

The failure mode to design against is silence. An authenticated person whose groups match no role gets no permissions at all — the platform logs a warning and lets them in to a system where nothing is reachable. They will report this as "the app is broken." It is the mapping being incomplete, and §11.8 is how you catch it before they do.

11.1 Step A — decide what carries the role, per identity mode

Identity modeWho asserts identityWhere roles come fromWhat you configure
Reverse proxy / SSO (recommended) Your IdP, in front of the app. PHI_AI_WEB_TRUST_PROXY_AUTH has no default — an operator must set it deliberately, because trusting headers on a directly-exposed port means anyone is whoever they type. Directory group membership, forwarded by the proxy in X-Auth-Request-Groups (identity in X-Auth-Request-User, X-Auth-Request-Email). Your proxy must set those three headers and strip any client-supplied copies. Rename them with PHI_AI_WEB_USER_HEADER / _EMAIL_HEADER / _GROUPS_HEADER if your proxy uses its own convention.
SMART-on-FHIR launch The EMR, at launch, from the issuer allowlist (PHI_AI_WEB_SMART_REDIRECT_URI). Still your directory. The launch establishes who and the patient context; it does not carry PHI AI roles, because EMR security classes and these permissions are not the same vocabulary. The same group mapping, keyed on the identity the launch asserts. Plan for a person who launches from inside the EMR but is grouped in your directory.
Local accounts (fallback only) The platform itself — the one mode where it holds a credential (PHI_AI_WEB_LOCAL_ACCOUNTS, mutually exclusive with proxy trust). authn.local_user_roles, constrained by a database CHECK that lists exactly the nine role strings. Roles per account, by hand (runbooks/RUNBOOK_LOCAL_USERS.md). No directory to inherit from, so recertification becomes a manual duty you must schedule.

11.2 Step B — inventory the EMR populations that will use this

Start from your EMR, because that is where your workforce is already segmented and the segmentation is usually right. The concept has a different name per vendor: Epic — user Template / SubTemplate and security classes; Oracle Health — position and its privileges; athenahealth, eClinicalWorks, MEDITECH, NextGen — role or permission group.

Two rules keep this tractable. Map only the populations who will use this platform — an EMR has hundreds of templates and you need the handful whose people open this app. And map job function, not seniority: a department chair who treats patients is a clinician here, because the title changes nothing about which permissions the work requires.

11.3 Step C — create one directory group per role, named for the role

Make the group the join key — one per role. The matcher is built for this: group names arrive comma- or semicolon-separated, are compared case-insensitively, and match on the trailing segment after the last - or :. So PHI-AI-HIM, phi-ai-him and role:him all resolve to the him role, and your IdP's naming convention does not have to change. Anything that resolves to no role is ignored, so unrelated groups in the same claim are harmless.

Do not map EMR templates straight to roles even though it looks like a shortcut: the group is what your access reviews enumerate, what joiner/mover/leaver automation writes to, and what an auditor can be handed as evidence. An EMR template is none of those things for this system.

11.4 Step D — the crosswalk

Assign each group exactly one role; compose people who genuinely do two jobs by putting them in two groups (§11.5). Permissions below are the effective sets the platform resolves, including assistant:use, which every role holds — the assistant is granted broadly precisely because it is not a way to see anything, since each of its tools declares the same permission the equivalent screen requires.

Who, in your organizationRoleEffective permissionsDeliberately withheld
Treating clinicians, residents, nursing — anyone who opens a chart to provide care viewer patient:search · patient:read · document:read · imaging:read · identity:search · assistant:use Cohort queries — a clinician finding the right patient is doing lookup, not research. Also the audit trail, psychotherapy notes and configuration.
Health information management, release-of-information staff him patient:search · patient:read · document:read · document:ingest · imaging:read · identity:search · analytics:query · roi:create · roi:export · integration:view · report:read · assistant:use Audit read — add auditor if the role reviews the trail. Also psychotherapy notes and configuration.
Privacy and compliance officers, internal audit auditor audit:read · audit:verify · report:read · assistant:ops · assistant:use All clinical content. An auditor is not a viewer with extras — they read the record of disclosures, never the records. assistant:ops lets them review how a PHI-touching capability is being used.
Population health and quality analysts analyst analytics:query · report:read · assistant:use patient:read and identity:search — counts without the ability to turn a cohort into a list of names. That gap is the whole distinction between analytics and disclosure, and closing it is the most common over-grant.
Research staff working record-level across many charts, under IRB or privacy-board approval researcher analytics:query · identity:search · research:search · patient:search · patient:read · document:read · report:read · assistant:use Imaging, audit, configuration. Granted and reviewed as one unit with its own purpose code — searching every chart at once is research, not care or counting.
Behavioral health clinicians (read §11.6 before granting) psychotherapy psychotherapy:read · assistant:use Everything else, including ordinary chart access. Held alone it opens nothing else, by design.
Platform operations and configuration owners admin admin:config · admin:users · analytics:query · integration:view · audit:read · report:read · assistant:ops · assistant:use Every clinical read. admin:users administers local accounts only: this person can create the user who reads a chart and cannot read one themselves. That separation is the point.
Records-retention and disposition decision-makers disposition retention:read · retention:dispose · retention:certificate · audit:read · report:read · assistant:use Clinical content. Disposal decisions run on holdings figures and the trail, not on reading the records being disposed.
Platform administrators — the smallest group you can defend sysadmin * Nothing. The only wildcard, and the only role carrying system:admin (the control panel). Full control is not exemption: every action lands on the audit trail under that person's own name.

11.5 Step E — compose roles; never invent one

People who do two jobs get two groups, and permissions union:

  • Behavioral-health clinician = viewer + psychotherapy — ordinary chart access plus the psychotherapy store.
  • Records professional who also reviews the trail = him + auditor.
  • Operations lead who also decides retention = admin + disposition.

If someone seems to need a role that does not exist, the answer is almost always a composition you have not tried — or a job that should be split between two people. It is never a new permission: permissions are enumerated in software, and screens check permissions rather than role names.

11.6 Step F — psychotherapy is a separate approval, always

No other role includes psychotherapy:read, and this is the mapping's most important property. Psychotherapy notes carry their own authorization regime (45 CFR §164.508(a)(2)) and their own storage boundary — a separate bucket under a separate key. The role is therefore its own grant holding that one permission and nothing else, deliberately not folded into viewer, him or researcher, so that granting any breadth of general clinical access never quietly includes the one record class the law treats differently. Give it its own directory group, its own approval step and its own reviewer — never fold it into the clinician group because behavioral-health staff are clinicians too.

11.7 Step G — set purpose-of-use expectations per role

Reading clinical content requires a stated reason, recorded in the audit entry. The permissions that demand one are patient:read, document:read, imaging:read, roi:export, research:search and psychotherapy:read. A role may assert only the purposes its work plausibly involves — the select offers only these, and the record-opening routes refuse a posted purpose outside them:

  • viewer — treatment, operations
  • psychotherapy — treatment
  • him — payment, operations, patient request, legal
  • researcher — research, operations
  • auditor, analyst, admin — operations
  • disposition — operations, legal

Train people on what their purposes mean before go-live. The purpose lands on every audit event, and a workforce that picks one at random makes the accounting of disclosures worth less. Note that the research purpose records which kind of access a read was — it does not establish that the IRB approval exists, which stays an organizational control.

11.8 Step H — verify the mapping before anyone relies on it

Do all four. The first three can pass while the fourth fails.

  1. Read the matrix. The Control panel (sysadmin only) shows every profile with its roles and the permissions those roles resolve to, live, from the same table the enforcement reads. It is an enforcement view, not a user directory — nobody is provisioned there.
  2. Sign in as a real person from each group, not as an administrator imagining them. Confirm the navigation shows what that role should have — and confirm it is not empty, which is the signature of a group that matched no role.
  3. Test the direct URL, not just the menu. A screen a role should not have must refuse the URL, not merely omit it from the nav. Hiding a link is presentation; the route check is the control. Paste a forbidden URL for each role and confirm the refusal.
  4. Read the audit trail. Every refusal writes access.denied with the permission that was missing. A mapping error shows up there as a pattern — one person refused the same permission repeatedly is usually a group they were never added to.

11.9 Step I — hand the lifecycle back to your directory

Once the map is in place, stop administering people here. Joiner / mover / leaver processing, access recertification and emergency revocation all happen in your identity system; the platform reads roles from the request rather than caching a local copy, so it reflects the result on the next request and there is no second role store to drift. In local-accounts mode none of that is true, which is the real reason it is a fallback rather than a choice.

11.10 The five mistakes this step exists to prevent

  • Everyone starts as sysadmin "just to get going." Pilots become production. Map the real roles on day one.
  • Psychotherapy folded into the clinician group. It hands the one category of record the law treats separately to every clinician at once.
  • Analysts given patient:read because a cohort screen "looked broken." It was working: counts without names is the feature.
  • Local accounts stood up alongside a working IdP, creating a second population nobody recertifies.
  • PHI_AI_WEB_DEV_PERSONAS left enabled. It fabricates identity and must never reach production — verify it is off as part of this step.

12. Step 7 — the assistant and your models

  1. Enable with PHI_AI_ASSISTANT_ENABLED, choose PHI_AI_ASSISTANT_PROVIDER = anthropic · bedrock · vertex, and set the provider credential (API key file or cloud IAM). Those three are the built-in shortcuts; any other provider — Azure AI Foundry, OpenAI, a hosted open-weights endpoint, or your own inference server — is a first-class option through the model registry below, which accepts any HTTPS endpoint. A cloud model service keeps traffic inside your BAA'd account; a vendor's API called directly needs its own agreement with that vendor for PHI-reading configurations (PHI_AI_ASSISTANT_PHI_ACCESS).
  2. Psychotherapy content access for the assistant is a separate, default-off switch (PHI_AI_ASSISTANT_PSYCHOTHERAPY_ACCESS).
  3. Bring your own models: register anything else — predictive, classifier, imaging, optimization, mapper — on the Control panel's registry (HTTPS endpoints only), then enable and activate into its capability slot; every lifecycle step is audited. The retrieval and live-call kill switches live on the same panel.
  4. Details: runbooks/RUNBOOK_AI_ASSISTANT.md and RUNBOOK_MODEL_GOVERNANCE.md.

13. Step 8 — first data

  1. Sandbox first. Run the first ingest cycle against your vendor's non-production FHIR sandbox — or the bundled EMR emulators (Emulators & non-PHI setup) — never a live EMR.
  2. Bulk: with the Group FHIR ID configured, the bulk import manager runs kickoff → poll → NDJSON download → encrypt-store-index; only a clean run advances the watermark.
  3. Streaming: connect ADT/results/remittance feeds per runbooks/RUNBOOK_EMR_EXCHANGE.md.
  4. Synthetic evaluation corpus: scripts/generate_corpus.py + scripts/load_local_corpus.py load a Synthea-generated population for evaluation without any real PHI.

14. Step 9 — verify before trusting it

  1. Audit chain: docker compose exec app python -m core.audit.verify — and the Control panel re-derives the chain on every load.
  2. Deletion detectability: against the sandbox, write a test object, delete it, confirm the surviving prior version and the entry in your cloud's access log (CloudTrail data events / GCS Data Access logs / Azure diagnostics). That is the integrity control — there is no storage lock to demonstrate.
  3. TLS end-to-end: EMR↔app, app↔storage, app↔KMS — confirmed in the Terraform plan's bucket policies before deployment.
  4. Permissions: walk each role against screens it must and must not reach; every refusal should appear in the audit trail as access.denied.
  5. The test suite: python -m pytest tests/ — 800+ tests, no cloud dependencies required.
  6. Full checklist: runbooks/RUNBOOK_VERIFICATION.md and RUNBOOK_HIM_VERIFICATION.md.

15. Promoting to production

  • require_mfa_to_assume_roles=true; real IdP in front; dev personas and dev identity removed; stable session secret from a secrets manager.
  • Production EMR enablement completed with the vendor (Epic: per-health-system subscription agreement and per-flavor write APIs).
  • Monitoring on the audit trail (denial patterns, system.* overrides, export refusals), backup and restore rehearsed (RUNBOOK_DATA_RESTORE.md), incident response wired (RUNBOOK_INCIDENT_RESPONSE.md).
  • Your organizational obligations — risk analysis, BAAs, consent capture, audit review — are enumerated in Compliance & responsibility; deployment does not discharge them.

16. The runbook shelf

Every operational topic has a runbook in runbooks/: cloud setup (AWS / GCP / GCP-zero-to-one / Azure), install checklist, web UI, local users, SMART launch, AI assistant, model governance, analytics, OMOP setup, EMR exchange, document ingestion, DICOM imaging, psychotherapy notes, retention rules, disposition, index maintenance, data restore, HIM verification, verification, incident response, and emulators for vendor-free integration testing. When this page and a runbook disagree, the runbook is newer — file the discrepancy.

Companions: Architecture for what you are wiring together, API details for the vendor auth flows, and Compliance for what remains your organization's duty after the install succeeds.