--- name: medplum-rules description: "Medplum (FHIR healthcare) coding rules: style, patterns, security, testing. Triggers: medplum.config.mts, medplum.config.ts, FHIR, Medplum, Bot, Subscription, Questionnaire." effort: medium user-invocable: false allowed-tools: Read --- # Medplum (FHIR healthcare) Rules These rules come from `app/rules/medplum/` in ai-toolkit. They cover the project's standards for coding style, frameworks, patterns, security, and testing in Medplum (FHIR healthcare). Apply them when writing or reviewing Medplum (FHIR healthcare) code. # Medplum / FHIR Coding Style ## Resource Structure - Every FHIR object must include `resourceType` as first field. - Use PascalCase for resource types (`Patient`, `ServiceRequest`), camelCase for fields (`birthDate`, `valueQuantity`). - Never hardcode resource IDs. Let the server assign them on create. - Include `meta.profile` when creating resources that must conform to a StructureDefinition. ## References - Use `createReference(resource)` from `@medplum/core` to build Reference objects. - Always include `display` on references for human readability. - Use `getReferenceString(resource)` for comparisons and logging — returns `ResourceType/id`. - Use `parseReference(ref)` to extract resourceType and id from a reference string. - Never concatenate strings to build references manually. ## CodeableConcepts & Coding - Always include `system`, `code`, and `display` in every Coding element. - Use standard terminology URIs: `http://loinc.org`, `http://snomed.info/sct`, `http://hl7.org/fhir/sid/icd-10-cm`. - Use `getCodeBySystem(cc, system)` to find codes; `setCodeBySystem(cc, system, code)` to set them. - Prefer `CodeableConcept` over plain `Coding` when the FHIR spec allows both — it supports multiple codings and free text. ## Identifiers - Use `identifier` arrays with `system` + `value` for external IDs (MRN, NPI, SSN). - Use `getIdentifier(resource, system)` and `setIdentifier(resource, system, value)` helpers. - Identifier systems must be absolute URIs (e.g., `http://hl7.org/fhir/sid/us-npi`). - Use `createResourceIfNoneExist(resource, 'identifier=system|value')` for idempotent creates. ## Extensions - Use the `extension` array with `url` and typed `value[x]` fields. - Prefer official HL7/US Core extensions over custom ones where they exist. - Use `getExtension(resource, url)` and `getExtensionValue(resource, url)` helpers. ## Bundles - Use `urn:uuid:` for internal references between entries in a transaction Bundle. - Every Bundle entry must have `request.method` (`POST`, `PUT`, `DELETE`) and `request.url`. - Include `fullUrl` on entries that are referenced by other entries. - Use conditional references (`Practitioner?identifier=npi|123`) for existing resources. ## HIPAA-Aware Coding - Identifiers like SSN, MRN, and insurance IDs are PHI — never log raw values to console or external services. - Use a safe logging utility (e.g., `safeLog()`) for any output that might contain patient data. Never `console.log` raw FHIR resources. - Reference `display` strings may contain patient names — treat as PHI in logs and error messages. - Every new data access path or admin operation must include corresponding `AuditEvent` creation. No exceptions. - When audit logging fails (Medplum unreachable), write to a fallback store — audit events must never be silently dropped. - See `security.md` rules for full HIPAA, access policy, and PHI handling requirements. ## Formatting - Use `formatHumanName()`, `formatAddress()`, `formatDate()`, `formatQuantity()` for display strings. - Use `getDisplayString(resource)` as a universal fallback for any resource's display name. - Never manually concatenate name parts — FHIR names have `given[]`, `family`, `prefix[]`, `suffix[]`. # Medplum Frameworks ## @medplum/core — SDK Client - Use `MedplumClient` for all FHIR operations. Never use raw `fetch` against Medplum endpoints. - Use `medplum.createResource()`, `readResource()`, `updateResource()`, `deleteResource()` for CRUD. - Use `medplum.searchResources()` for typed arrays. Use `medplum.searchOne()` when expecting a single result. - Use `medplum.executeBatch()` for transaction Bundles — groups multiple operations atomically. - Use `medplum.upsertResource(resource, query)` for atomic create-or-update. - Use `medplum.createResourceIfNoneExist(resource, query)` for idempotent creation. - Configure `autoBatchTime` on MedplumClient to auto-batch concurrent GET requests. Use `Promise.all()` instead of sequential `await` to benefit from batching. ## @medplum/fhirtypes — Type Safety - Import FHIR types directly: `import { Patient, Observation } from '@medplum/fhirtypes'`. - Use TypeScript types for all FHIR resources — never use `any` for resource data. - Cast `event.input` in bot handlers: `const patient = event.input as Patient`. - Use optional chaining for nested FHIR fields: `patient.name?.[0]?.given?.[0]`. ## @medplum/react — UI Components - Wrap app with `` at the root. - Use `useMedplum()` hook to access the MedplumClient instance in components. - Use `useMedplumContext()` for client + profile + loading state together. - Use `` for auto-generated CRUD forms, `` for display. - Use `` for searchable/filterable resource lists. - Use `` to render FHIR Questionnaires and capture responses. - Use `useSubscription(criteria)` for real-time WebSocket data in React components. - Requires Mantine 7+ and PostCSS with Mantine preset. Import `@mantine/core/styles.css`. ## Bot Development - Export a single `handler` function: `export async function handler(medplum: MedplumClient, event: BotEvent)`. - Access trigger resource via `event.input`. Access secrets via `event.secrets`. - Use `event.contentType` to determine input format (`application/fhir+json`, `text/plain`, `x-application/hl7-v2+er7`). - Deploy bots via CLI for CI/CD: `medplum bot deploy `. - Apply AccessPolicies to bots — restrict to minimum required resource types. - Use Subscriptions with `channel.type: 'rest-hook'` and `channel.endpoint: 'Bot/'` for event-driven execution. ## GraphQL - Append `List` to resource type for searches: `PatientList(name: "Eve")`. - Use snake_case for search parameters in GraphQL (not kebab-case): `address_city`, not `address-city`. - Use inline fragments for reference resolution: `... on Observation { valueQuantity { value } }`. - Use `_reference` for reverse lookups: `EncounterList(_reference: patient)`. ## CLI (@medplum/cli) - Use `medplum login` for auth, `medplum get`/`medplum post` for FHIR operations. - Use `medplum bot deploy` for bot deployment in CI/CD pipelines. - Use `medplum bulk export` for bulk data operations. # Medplum / FHIR Patterns ## Bundle Transactions - Use `type: 'transaction'` for atomic multi-resource operations — all-or-nothing. - Use `type: 'batch'` when operations are independent and partial failure is acceptable. - Use `urn:uuid:` in `fullUrl` for forward references between entries. - Reference other entries via `{ reference: 'urn:uuid:' }`. - Use `ifNoneExist` on POST entries for conditional creation (idempotent). - Use `ifMatch: 'W/"versionId"'` on PUT entries for optimistic concurrency. - Use conditional references for existing resources: `Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|123`. - For large bundles (>50MB), use `Prefer: respond-async` header. ## Search Patterns - Use `_include=ResourceType:searchParam` to fetch referenced resources in one call. - Use `_revinclude=ResourceType:searchParam` to fetch resources referencing your results. - Use `:iterate` modifier for multi-hop traversal: `_include:iterate=Patient:general-practitioner`. - Use `_count` + `_offset` for pagination; use `searchResourcePages()` for async iteration. - Use `:contains` modifier for substring search on string params: `name:contains=eve`. - Use `:not` modifier to exclude: `status:not=completed`. - Use comma-separated values for OR: `status=active,on-hold`. - Use multiple parameters for AND: `name=Simpson&birthdate=1940-03-29`. - Prefer `searchResources()` over `search()` — returns typed array, not raw Bundle. ## Subscription & Bot Workflows - Create a `Subscription` resource with `criteria` (FHIR search query) and `channel.type: 'rest-hook'`. - Point `channel.endpoint` to `Bot/` for automated processing. - Use `subscribeToCriteria()` client-side for WebSocket real-time updates. - Never subscribe to `AuditEvent` changes — prevents notification spirals. - Use cron-based bots for scheduled tasks (e.g., daily reports, batch processing). ## Access Policies - Define `AccessPolicy.resource[]` with `resourceType` and optional `criteria`, `readonly`, `hiddenFields`, `readonlyFields`. - Use `%profile` variable to scope data to the current user: `Observation?performer=%profile`. - Use `%patient` variable for patient-portal access: `Observation?subject=%patient`. - Use compartment-based access for patient-scoped isolation. - Use `writeConstraint` with FHIRPath for state machine enforcement (e.g., prevent status rollback). - Apply least privilege: start with no access, add specific resource types. ## Conditional Operations - Use `createResourceIfNoneExist(resource, query)` for idempotent creates keyed on identifier. - Use `upsertResource(resource, query)` for atomic create-or-update in a single request. - Use `If-None-Exist` header on POST for server-side conditional creation. ## Patient Deduplication - Match on `identifier` systems (MRN, SSN, insurance ID) for deterministic matching. - Use probabilistic matching on name + birthdate + address for fuzzy matches. - Use Patient `link` field with `type: 'replaced-by'` for merge workflows. - Prefer `createResourceIfNoneExist()` at ingestion to prevent duplicates. ## Questionnaire Workflows - Create `Questionnaire` resources for form definitions. Use `linkId` for question identification. - Use `QuestionnaireResponse` for captured answers. Link to Questionnaire via `questionnaire` field. - Use `getQuestionnaireAnswers(response)` to extract answers as a map keyed by `linkId`. - Automate post-submission processing with a Bot subscribed to `QuestionnaireResponse` creation. - Use SDC (Structured Data Capture) extensions for advanced rendering and extraction. # Medplum / FHIR Security ## Authentication - Use client credentials flow (`startClientLogin`) for backend services and integrations. - Use authorization code flow (`startLogin` + `processCode`) for user-facing web apps. - Never store tokens in `localStorage` in production — use secure HTTP-only cookies or server-side sessions. - Use `refreshIfExpired()` before critical operations. Set `gracePeriod` to refresh proactively. - Use `setBasicAuth(clientId, clientSecret)` only for server-side code, never in browser. - Rotate client secrets via `$rotate-client-secret` operation periodically. ## Access Policies - Every non-admin user must have an AccessPolicy. Never leave users with default full access. - Scope to specific resource types: list only the types the user needs. - Use `readonly: true` or explicit `interaction` arrays to restrict write access. - Use `criteria` with FHIR search syntax to filter visible resources (e.g., `Patient?organization=Organization/123`). - Use `hiddenFields` to prevent sensitive fields from being returned (e.g., SSN). - Use `readonlyFields` to allow viewing but prevent modification of specific fields. - Use `writeConstraint` FHIRPath expressions for business rules (e.g., prevent status rollback on finalized resources). - Test access policies by logging in as a test user with the policy applied. ## HIPAA & Audit Logging - Medplum automatically creates AuditEvent resources for all FHIR operations. - Never log PHI (patient names, identifiers, health data) to application console or external services. - Use structured audit references: reference the Patient and the accessing Practitioner in audit records. - For custom audit trails, create AuditEvent resources with `type`, `agent`, `entity`, and `outcome`. - Ensure audit events are never silently dropped — if Medplum is unreachable, write to a fallback store. ## PHI Handling - Never include PHI in URLs, query parameters, or HTTP headers. - Use POST-based search for queries containing sensitive criteria. - Use `Binary` resources with `securityContext` for sensitive file attachments. - Encrypt data at rest and in transit (TLS 1.2+). Medplum hosted handles this automatically. - Apply data retention policies — use `$expunge` operation for permanent deletion when required. ## SMART Scopes - Use `patient/*.read` style scopes for patient-facing apps. - Use `user/*.read` style scopes for practitioner-facing apps. - Validate scopes server-side on every request — do not trust client-side scope claims. - Use `launch/patient` context for apps launched within a patient context. - Define minimal scopes: request only the resource types and operations needed. ## Multi-Tenant Isolation - Use Medplum Projects for tenant isolation — each project is a separate data silo. - Never share AccessPolicies across projects/tenants. - Validate `meta.project` on operations when building multi-tenant middleware. - Use separate ClientApplications per tenant for backend integrations. ## Secrets Management - Use Bot secrets (`event.secrets`) for API keys, connection strings, and credentials. - Never hardcode secrets in bot source code or resource data. - Use Medplum project-level secrets storage — accessible only by project admins. - Rotate secrets on a regular schedule and after any suspected compromise. # Medplum / FHIR Testing ## MockClient - Use `MockClient` from `@medplum/mock` for unit tests — it simulates the full MedplumClient API in memory. - Pre-populate test data with `mockClient.createResource()` before running test assertions. - MockClient supports `search`, `searchResources`, `searchOne`, `readResource`, `updateResource`, `deleteResource`. - MockClient does not require network access — tests run fast and offline. - Use `new MockClient()` per test to ensure isolation between test cases. ## Bot Unit Testing - Test the handler function directly: `await handler(mockClient, mockEvent)`. - Create mock `BotEvent` objects with `input`, `contentType`, `secrets`, and `bot` fields. - Verify resource creation: call `mockClient.searchResources()` after handler execution. - Test error paths: pass invalid input resources and assert the handler throws or returns errors. - Test different content types: `application/fhir+json`, `text/plain`, `x-application/hl7-v2+er7`. - Mock `event.secrets` for bots that depend on external API keys. ## Resource Validation - Use `validateResource(resource)` from `@medplum/core` to check resources against StructureDefinitions. - Test that required fields produce `OperationOutcome` errors when missing. - Test custom profiles: create a `StructureDefinition` resource, then validate resources against it. - Use the `$validate` operation for server-side validation in integration tests. - Test the Data Absent Reason extension when required fields may legitimately be empty. ## Search Testing - Test search parameter behavior: exact match vs prefix match vs substring (`name`, `name:exact`, `name:contains`). - Verify `_include` returns related resources in the Bundle. - Test pagination with `_count` and `_offset` parameters. - Test token search with and without system namespace: `identifier=value` vs `identifier=system|value`. - Test date range searches with comparison prefixes: `ge`, `le`, `gt`, `lt`. ## Integration Testing - Use Medplum Docker image (`medplum/medplum-server`) for local integration tests. - Test full workflows end-to-end: create patient → create observation → search → verify. - Verify Bundle transactions are atomic: intentionally fail one entry and confirm rollback. - Test access policies by authenticating as users with different policies. - Test Subscription triggers: create a Subscription, modify a matching resource, verify Bot execution. ## Test Data Factories - Create typed factory functions: `createTestPatient(overrides?)`, `createTestObservation(overrides?)`. - Use realistic but synthetic data — never use real patient data in tests. - Include only minimal required fields by default. Let tests add specific fields via overrides. - Use `generateId()` from `@medplum/core` for unique test identifiers. - Use standard test identifier systems: `http://example.com/test-mrn` to avoid collision with real systems. ## Assertions - Assert on `OperationOutcome` for error responses: check `issue[].severity`, `issue[].code`, `issue[].expression`. - Use `isOk(outcome)` and `isNotFound(outcome)` from `@medplum/core` for status checks. - Use `deepEquals(a, b)` for resource comparison (ignores `meta.versionId` and `meta.lastUpdated`). - Assert reference integrity: verify `subject.reference` matches expected `Patient/id` format.