Pery Demomock data
Home
Back to Welcome
For engineers

What we built, in plain English.

Pery is a multi-tenant SaaS for mental-health clinics. Three user-facing apps — a clinician web app, a patient web app, and a patient mobile app for iOS and Android — all written in TypeScript and sharing code through a single monorepo. A Node.js backend serves the API and runs background jobs.

A separate Python service — the EMR bridge — speaks to 11 different electronic-health-record systems through one unified API. Adding a 12th EHR is a connector module, not new product code. That's the moat.

What an acquirer gets: production-grade infrastructure (AWS + Cloudflare, with Pulumi-managed IaC), real multi-tenancy at the database and middleware layer, RBAC auth via AWS Cognito, observability through Sentry and Coralogix, automated CI/CD, and a clean separation between product code and shared libraries.

View on GitHubRepo is private — request access from Dan to browse the code.
The surfaces

Four products, one codebase

Three things the user sees, plus the engine room.

Clinician web app

packages/web

Main product. Next.js 15 + React 19. Where MHEs run their day.

Patient web app

apps/bhi-web

Public assessment links, magic-link auth, voice check-ins.

Patient mobile app

apps/bhi

React Native + Expo. iOS and Android from one codebase.

Server + EMR bridge

packages/server · packages/emr-bridge

Node REST API + Python service that connects to 11 EHRs.

By the numbers

At a glance

4
user-facing apps
4
shared packages
17
API route domains
11
EHR systems
30+
external services
2
migration tracks (global + per-tenant)
Deep dive
Stack

What it's built with

Every group is a real dependency in the codebase, not a wish list.

Languages
TypeScript (primary)Python (EMR bridge)SQL
Frontend
Next.js 15 App RouterReact 19Tailwind CSS 3/4shadcn/ui + RadixMUI (date pickers)TanStack Query v5TanStack Table v8ZustandRecharts@react-pdf/rendererMotion
Mobile
React Native 0.76Expo 52Expo RouterReact NavigationAWS AmplifyTwrnc (Tailwind RN)Lottie · Sentry · Firebase
Backend
Express.jsSequelize (runtime)Prisma (introspection)Zod + AJV validationSwagger / OpenAPIWinston loggingUmzug migrations
Data
PostgreSQL (RDS)S3SNS / SQSCloudinary
Auth
AWS CognitoJWTCustom RBAC middlewareSubdomain → tenant
AI & comms
OpenAIAWS Polly (TTS)Twilio (SMS)SendGrid (email)Customer.ioExpo + Firebase + web-pushPubNub (realtime)
Infra & ops
AWS Elastic BeanstalkECSCloudflare WorkersPulumi (IaC)Docker (multi-stage)GitHub ActionsSentry · Coralogix · Segment
Testing
Jest 29 (web/server/mobile)Vitest (Cloudflare Workers)pytest + mypy (EMR bridge)supertestjest-junit
Tooling
pnpm workspacesTurborepoESLint + PrettierTS strict + noUncheckedIndexedAccess
Layout

Repository structure

A pnpm + Turborepo monorepo. Apps consume shared packages; nothing is duplicated.

apps/
bhi-web/— Next.js — patient-facing web (assessments, auth)
bhi/— React Native + Expo — patient mobile (iOS/Android)
demo/— this site — partner showcase
packages/
web/— Next.js 15 — main clinician app
server/— Express REST API + worker mode
common/— Shared services: logging, S3, SMS, email, push, postgres
schemas/— Zod schemas + types shared by web & server
emr-bridge/— Django / Python — 11-EHR uniapi connector
cloudflare/— Edge Workers (DNS, S3 proxy, Apple App Site Assoc.)
migrations/
global/— Tenant-independent schema (Umzug + raw SQL)
tenant/— Per-tenant schema (one DB schema per organization)
configuration/— Per-environment YAML (local/dev/test/staging/prod)
Backend

Server architecture

Express, layered cleanly, with the same binary running in two modes.

Layered server (packages/server)

  • Layers: Routes → Business Logic ( bl/, ~30 domain modules) → Models / Services
  • Middleware chain: CORS → Morgan → Body parser (50MB) → HTTP context → JWT parser → Organization context → Role authorizer → Routes → Error handler
  • Two run modes via IS_WORKER: API server vs. background worker (cron, SMS queues, notifications, weekly quick-share, app-signup reminders).
  • Validation: Zod (modern paths) + AJV (legacy paths).
  • API docs: Swagger UI at /api-docs in non-prod.
API route domains
/auth/patients/assessments/assessments-templates/experts/organization/organization-config/dynamic-config/admin/acknowledge/assessments-views/note-templates/sms/webhook/bhiReport

Multi-tenancy & data

  • One PostgreSQL database, two migration tracks: global (one schema) and tenant (one schema per organization).
  • organization_id is injected into the request context by middleware. Every query is scoped to it.
  • Subdomain routing — {org-slug}.mypery.com maps to the org.
  • Two ORMs: Sequelize for runtime queries, Prisma for schema.prisma introspection and type generation.
Core models
userspatientsassessmentsassessment_templatesmhesacknowledgmentsnotificationssubscriptionsreportsschedulessmssms_conversationssms_queuesummary_notestime_logstranslationspatient_registrationsnote_templates
Auth

Authentication & RBAC

One JWT, three roles, enforced at the middleware boundary on the server.

  1. 1Sign up with phone number — Twilio sends SMS verification.
  2. 2AWS Cognito issues the JWT. Claims include user_id, organization_id, role.
  3. 3Server enforces: jwt-parser-middleware → organization-middleware → role-authorizer.
  4. 4Web validates session via Amplify's fetchAuthSession in Next.js middleware; subdomain selects the org.
  5. 5Mobile uses AWS Amplify with AsyncStorage for token persistence.
OWNERADMIN — full accessUSER — clinicianPATIENT — limited
The moat

EMR bridge

One unified API across 11 different EHRs.

  • Separate Django / Python service with Celery for async jobs.
  • A unified API layer (uniapi/) abstracts each EHR's quirks. Adding a 12th is a connector module, not product surgery.
  • Tested with pytest, type-checked with mypy, containerized separately.
Supported EHRs (11)
AllscriptsAthenaCerboCernerDrChronoeClinicalWorksElationEpicNexTechNextGenOpenEMR
Infra

Infrastructure & operations

Real cloud setup, not a developer's laptop.

Cloud (AWS)

Cognito · Secrets Manager · S3 · CloudFront · Elastic Beanstalk · RDS Postgres · API Gateway · SNS · SQS · ECS · Polly.

Edge

Cloudflare Workers handle DNS validation, Apple App Site Association, and S3 proxying.

IaC & containers

Pulumi for infrastructure-as-code. Docker multi-stage builds on Node.js 20. docker-compose with an nginx reverse proxy for local + nodes.

CI / CD

GitHub Actions runs build + tests on every push. Staging auto-deploys nightly at 01:00 UTC from the trunk. Production is a manual promote of an already-staged build.

Observability

Sentry for error tracking. Winston → Coralogix for logs. Segment for product analytics. PII is filtered before it ever leaves the process.

Hygiene

What this codebase does well

The boring things that compound — useful in due-diligence.

  • TypeScript strict mode, including noUncheckedIndexedAccess across the whole repo.
  • Prettier-enforced formatting (Tailwind class sorting included). One style, no debate.
  • Turborepo pipeline runs build → test in dependency order; jest-junit reports merged into a single CI artifact.
  • Trunk-based development: feature branch → PR → ≥1 approval → merge to main.
  • HIPAA-aware: no PII in logs, errors, or analytics. Multi-tenant scoping enforced at the middleware boundary, not in app code.
  • Zod schemas live in @periapt/schemas and are imported by both web and server, so the wire format is type-safe end-to-end.
  • All credentials in AWS Secrets Manager. Environment is selected at runtime via with-config.sh + per-env YAML.
  • Two ORMs by design: Sequelize handles runtime queries; Prisma is used purely for schema introspection and type generation.
github.com/danlanda/peryCLAUDE.mdPrivate repo — access by request.
This page is a static summary of the production codebase, generated from CLAUDE.md at the repository root — the living source of truth for engineers. The demo site itself (apps/demo) is a separate, intentionally backend-free Next.js app. None of the production code, credentials, or PHI is reachable from here.