YesYou Health is a Vercel-ready Next.js application that lets a patient select a supported healthcare organization, authorize access to an Epic/MyChart record, and download the health data exposed by that organization. UCSF Health is selected by default, and Sutter Health is also available.
The data flow is browser-only:
- The browser discovers the healthcare organization's SMART endpoints, creates OAuth state and an S256 PKCE pair, and redirects to MyChart.
- The patient signs in at the healthcare organization and approves read-only access.
- The organization redirects to the static
/callbackpage with a short-lived authorization code. - The browser validates state, exchanges the code using PKCE, and retrieves the authorized FHIR record directly from the organization.
- After authorization, the patient creates a storage passphrase. The browser derives a 256-bit key with Argon2id and encrypts every FHIR resource and selected clinical-note file with AES-GCM before writing ciphertext to IndexedDB. After the staged import completes, it becomes the current local record and
/exploreopens. The patient can review rendered fields, inspect raw FHIR JSON, download a decrypted export, lock the record, or remove it. The access token, passphrase, encryption key, and health records do not pass through the YesYou Health application server.
Configure the patient-facing Epic app as follows:
- FHIR version: R4
- SMART scope version: SMART v2
- Audience: Patients
- Automatic client distribution: USCDI v3
- Confidential client: No, for the default public PKCE configuration
- Dynamic clients: No
- Redirect/Endpoint URI locally:
http://localhost:3000/callback - Redirect/Endpoint URI in production:
https://YOUR_DOMAIN/callback
Select the R4 read/search APIs used in lib/epic.ts, especially:
- Patient.Read (Demographics)
- ExplanationOfBenefit.Search (Prior Auth)
- AllergyIntolerance.Search (Patient Chart)
- Appointment.Search variants for appointments and scheduled surgeries
- CareTeam.Search (Longitudinal CareTeam)
- Condition.Search variants needed for the patient chart
- Coverage.Search (Patient Insurance Information)
- DeviceUseStatement.Search
- DiagnosticReport.Search (Results)
- DocumentReference.Search (Clinical Notes)
- Binary.Read (Clinical Notes), if clinical-note files should be imported
- Encounter.Search (Patient Chart)
- FamilyMemberHistory.Search
- Goal.Search (Patient)
- Immunization.Search (Patient Chart)
- MedicationDispense.Search (Fill Status)
- MedicationRequest.Search (Signed Medication Order)
- Observation.Search variants for labs, vitals, and social history
- Procedure.Search variants for orders and surgeries
- QuestionnaireResponse.Search (Patient-Entered Questionnaires)
- ServiceRequest.Search (Orders)
The exporter sends Epic's required search filters: longitudinal CarePlans use
category 38717003, while Observations are fetched and merged across the
laboratory, vital-signs, and social-history categories.
Prior authorization data is optional because it is exposed through a separately
enabled Epic API. If an import reports that the client is not authorized for
ExplanationOfBenefit - Prior Auth, enable ExplanationOfBenefit.Search (Prior
Auth) for that client in Epic on FHIR and then perform a new import. A SMART
scope alone cannot grant access to an API that is not enabled in the client
registration.
The public documentation URL can point to the deployed home page. Terms are at /terms and the privacy notice is at /privacy.
The app has a small Postgres-backed account tier (next-auth v5, direct SQL via
pg — no ORM):
- Sign-in: email + password (Argon2id-hashed) or Google. Google users pick
their role and fields on
/onboardingafter first sign-in. - Roles:
patientordoctor, chosen at signup and immutable afterwards. - Fields: a seeded list of medical fields (Diabetic Care, GI Health, Cardiology, …). Every user selects up to two.
- Patients import their MyChart record (the browser-only encrypted flow below, which needs no account) and can browse literature. Accounts gate the patient dashboard, field selections, and doctor tools — never the import.
- Doctors add peer-reviewed literature (title, authors, journal, year, and
a DOI or PubMed link) to their own fields via
/doctor. Everyone can browse it at/literature.
Health records never touch the server: the backend stores only accounts, field selections, and literature citations.
Schema lives in migrations/*.sql, applied by pnpm db:migrate
(scripts/migrate.ts, tracked in schema_migrations). Server code is under
lib/server/ (db.ts pool, password.ts, validation.ts, users.ts,
fields.ts, literature.ts) with the next-auth config in auth.ts and route
protection in proxy.ts.
Requires Node.js 24 or newer and PostgreSQL 14 or newer.
pnpm install
cp .env.example .env.localSet the non-production Epic client ID, DATABASE_URL, and a generated
AUTH_SECRET (openssl rand -base64 32) in .env.local. For Google sign-in,
also set AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET from a Google OAuth client with
redirect URI http://localhost:3000/api/auth/callback/google. Then run:
createdb yesyou
pnpm db:migrate
pnpm devOpen http://localhost:3000.
Development includes Epic Sandbox as an additional organization choice and labels it as synthetic test data. UCSF remains selected by default.
Supported organizations are allowlisted in lib/providers.ts. Each profile
contains the organization-facing name, portal name, FHIR R4 base URL, adapter,
environment, capabilities, and optional OAuth client ID or scope overrides.
The shared EPIC_CLIENT_ID and EPIC_SCOPE values are used when a profile does
not override them.
When adding or changing a production profile:
- Confirm the patient-facing R4 base URL with the organization or Epic's production endpoint directory.
- Verify SMART discovery plus browser CORS for discovery, token exchange, and authorized FHIR requests.
- Configure capability flags for optional attachments and prior authorization searches.
- Increment
PROVIDER_REGISTRY_VERSION. OAuth transactions created against an older registry will then safely restart instead of changing organizations mid-authorization.
pnpm test
pnpm typecheck
pnpm buildDatabase integration tests run only when TEST_DATABASE_URL is set (they
migrate and truncate that database):
createdb yesyou_test
TEST_DATABASE_URL=postgres://localhost:5432/yesyou_test pnpm testThe included TypeScript CLI accepts plain or gzip-compressed JSON containing a FHIR Bundle, a single FHIR resource, an array of resources, or a YesYou grouped export. It writes a concise Markdown record with a table of contents, patient name and date of birth shown exactly once, and a reverse-chronological clinical record. Resources linked to an encounter are nested beneath that encounter; the remaining records are grouped by clinical category and date of service. Notes, AVS/encounter summaries, and other document attachments are included when their payloads are embedded in the input, or clearly marked as referenced but unavailable. Resource metadata, identifiers, patient references, empty values, and duplicate resources are not included.
pnpm fhir:markdown -- patient-export.json patient-summary.mdFor a .json.gz input, the default output removes both extensions:
pnpm fhir:markdown -- patient-export.json.gz
# Writes patient-export.mdThe output path defaults to the input filename with a .md extension. Standard
input and a custom document title are also supported:
cat patient-export.json | pnpm fhir:markdown -- - patient-summary.md --title "Clinical Summary"Do not commit secrets. Add these environment variables in Vercel:
EPIC_CLIENT_ID=your-production-client-id
EPIC_REDIRECT_URI=https://YOUR_DOMAIN/callback
EPIC_SCOPE=openid fhirUser launch/patient patient/*.read
NEXT_PUBLIC_SUPPORT_EMAIL=your-support-address
DATABASE_URL=your-hosted-postgres-connection-string
AUTH_SECRET=generate-with-openssl-rand-base64-32
AUTH_GOOGLE_ID=your-google-oauth-client-id
AUTH_GOOGLE_SECRET=your-google-oauth-client-secret
Register https://YOUR_DOMAIN/api/auth/callback/google as an authorized
redirect URI in the Google OAuth client, and run pnpm db:migrate against the
production database as part of each deploy that adds a migration.
The client ID is a public identifier and is embedded in the browser application. Do not configure EPIC_CLIENT_SECRET: this implementation is a public PKCE client, and browser code cannot keep a client secret confidential.
After establishing the final Vercel domain, register the exact production callback with Epic before marking the client Ready. Preview deployment domains will not work unless each preview callback URI is separately registered.
The selected healthcare organization must permit browser cross-origin requests to its SMART discovery, token, and FHIR endpoints. UCSF and the Epic sandbox currently allow the required origins, methods, and Authorization/Content-Type headers. The application explicitly omits cross-origin cookies and other credentials from these requests.
- The Explore page creates a
.json.gzdownload on request. Stored Binary files are encoded into FHIR Binary resources only while preparing that download. Browsers without the standardCompressionStreamAPI receive an uncompressed.jsonfile instead. - The export includes raw FHIR resources and may contain highly sensitive health information.
- OAuth state and the PKCE verifier are kept in browser session storage for at most ten minutes and removed on callback.
- The access token is processed only in browser memory and is never saved. A new encrypted import is staged under a unique ID and replaces the previous import only after it completes successfully.
- The storage passphrase must contain at least 12 characters. It is normalized with NFKC and passed to Argon2id with a unique 16-byte salt, 19 MiB of memory, two iterations, and one lane to derive a 256-bit AES key. The passphrase is never stored and the non-extractable key exists only in browser memory.
- Structured resources, document metadata, errors, attachment metadata, and attachment bytes are authenticated and encrypted with AES-256-GCM using a fresh 96-bit IV and record-specific additional authenticated data. IndexedDB contains only ciphertext plus the non-secret key-derivation parameters, random record identifiers, completion state, and the current-record pointer.
- Database schema upgrades delete records created by older plaintext-storage versions. They are not migrated or opened.
- Optional clinical-note files retain a 10 MB per-file limit, 50 MB aggregate limit, and a supported HTML, text, RTF, PDF, or image content type before encryption.
- The app checks the browser's estimated quota and requests persistent storage. The browser may deny that request or remove non-persistent data under storage pressure, and users can always clear site data.
- Downloaded
.jsonand.json.gzexports are decrypted files and are not protected by the browser-storage passphrase. - The callback removes the authorization code from the address bar immediately and the site sends
Referrer-Policy: no-referrer. The initial callback page request still reaches the hosting provider and may appear in limited technical logs. - FHIR pagination is restricted to the configured provider origin and API base.
- Binary URLs must remain inside the configured FHIR base and identify a direct
Binary/{id}resource. Imported HTML is stored but never injected into the application page. - Do not add analytics, session replay, third-party scripts, or client-side error reporting to the authorization/export pages without a privacy and security review.
- The legal copy is a product-ready starting point, not legal advice. Have counsel review the Terms and Privacy Notice before handling production health data.