This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Ottehr is an open-source, production-ready EHR built on the Oystehr backend platform. It has three main components:
apps/intake— Patient-facing portal (React, runs on port 3002)apps/ehr— Provider-facing EHR (React, runs on port 4002)packages/zambdas— Backend endpoints deployed as Oystehr Zambdas (serverless functions), run locally via an Express emulator on port 3000
This is a Turborepo + npm workspaces monorepo. Key packages:
packages/utils— Shared types, FHIR helpers, constants, and utilities used by both apps and zambdaspackages/ui-components— Shared React components (MUI-based)packages/zambdas— All backend endpointspackages/spec— Versioned schema definitions for IaC config generationpackages/config-types— Shared configuration TypeScript typesconfig/oystehr/— Static IaC configuration files (JSON) for Oystehr resources (zambdas, apps, roles, FHIR resources)deploy/— Terraform deployment scripts and configuration
npm run apps:start:no-apply # Starts intake, EHR, and zambdas (skips terraform apply)
npm run apps:start # Same but runs terraform apply-local firstnpm run ehr:start # EHR frontend only
npm run intake:start # Intake frontend only
npm run zambdas:start # Zambdas local server onlynpm run build # Build everything via turbo
npm run ehr:build # Build EHR only
npm run intake:build # Build intake onlynpm run lint # Lint all packages
npm run lint:fix # Auto-fix lint issues
npm run ehr:lint # Lint EHR and its dependenciesnpm test # Run all unit/component tests via turbo
# EHR unit tests (vitest)
cd apps/ehr && npm run unit-tests
# EHR component tests (vitest + jsdom)
cd apps/ehr && npm run component-tests
# Run a specific EHR component test file
npx vitest --config ./apps/ehr/vitest.config.component-tests.ts apps/ehr/tests/component/MyTest.test.tsx
# Zambda unit tests
cd packages/zambdas && npx vitest run test/ # exclude integration/
# Zambda integration tests (requires local server running)
cd packages/zambdas && npx vitest run test/integration/npm run ehr:e2e:local # EHR E2E tests
npm run ehr:e2e:local:ui # EHR E2E with Playwright UI
npm run ehr:e2e:local:integration # EHR E2E in "fast mode" (single FHIR transaction)
npm run intake:e2e:local # Intake E2E tests
npm run intake:e2e:local:ui # Intake E2E with Playwright UIE2E tests for EHR live in apps/ehr/tests/e2e/specs/; for intake in apps/intake/tests/specs/.
To create a test appointment for Playwright MCP testing:
npx tsx apps/ehr/tests/e2e-utils/create-test-appointment.ts [in-person|telemed]cd packages/zambdas && npm run bundle # Bundles all zambdas to .dist/ as zip filesThen upload zips manually to the Oystehr console if needed.
npm run bump-canonical-version config/oystehr/<file>.json [minor|major|patch] # Version canonical FHIR resourcesEach endpoint in packages/zambdas/src/ehr/ or packages/zambdas/src/patient/ follows the same pattern:
index.ts— Lambda handler, exportsindexfunction usingwrapHandler()validateRequestParameters.ts— Input validationhelpers.ts— Business logic helpers
Zambdas use wrapHandler from ../../shared/lambda for consistent error handling. They authenticate via machine-to-machine (M2M) tokens cached in module scope across warm invocations.
The local server (packages/zambdas/src/local-server/index.ts) reads config/oystehr/zambdas.json to register all routes as Express endpoints, enabling local development without deploying.
Both apps are Vite + React + TypeScript + MUI. State management uses Zustand stores. API calls go through apps/ehr/src/api/api.ts (or equivalent in intake), which dispatches to zambda endpoints via @oystehr/sdk.
EHR feature structure follows apps/ehr/src/features/<feature>/:
in-person/andtelemed/subdirectories for visit-type-specific logicstore/— Zustand stores (e.g.,parsedAppointment.store.ts)hooks/— React Query hooks and data-fetching logicqueries/— React Query definitionscomponents/— Feature-specific componentspages/— Route-level page components
FHIR Appointment data flow (EHR): Raw FHIR bundle → parseBundle() in apps/ehr/src/features/visits/shared/stores/appointment/parser/parser.ts → two separate Zustand stores (raw for telemed compatibility, parsed for new in-person code).
utilsis imported by both frontend apps and zambdas. It exports FHIR helpers, types, secrets utilities, and constants frompackages/utils/lib/main.ts.ui-componentsis imported by EHR and intake for shared MUI-based components.
Static Oystehr resource definitions live in config/oystehr/ as JSON files validated by schema versions defined in packages/spec/. The deploy/ directory contains Terraform scripts that consume these configs to provision environments. Schema version 2025-09-25 is current.
Canonical FHIR resources (referenced by URL + version) must be versioned using npm run bump-canonical-version before modification—never edit them in-place.
Feature flags are defined in packages/utils/lib/ottehr-config/feature-flags/index.ts and validated against the Zod schema in packages/config-types/config/feature-flags.ts. The EHR re-exports them at apps/ehr/src/constants/feature-flags.ts.
- Component tests (
*.test.tsx) run in jsdom via vitest; located inapps/ehr/tests/component/ - Unit tests (
*.test.ts) run in node via vitest - Zambda integration tests require the local server running; use
setupIntegrationTest()frompackages/zambdas/test/helpers/integration-test-setup.tswhich sets up a full FHIR appointment graph and cleans up after - E2E tests (Playwright,
*.spec.ts) use credentials fromapps/ehr/env/tests.local.json
- Pre-commit hook runs
lint-staged - A custom git merge driver resolves
package.json/package-lock.jsonversion conflicts by picking the higher version—run./scripts/setup-git-merge-driver.shto install it config/andapps/ehr/tests/e2e-utils/seed-data/use the "ours" merge strategy to preserve local versions during upstream merges