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.
Four products, one codebase
Three things the user sees, plus the engine room.
Clinician web app
packages/webMain product. Next.js 15 + React 19. Where MHEs run their day.
Patient web app
apps/bhi-webPublic assessment links, magic-link auth, voice check-ins.
Patient mobile app
apps/bhiReact Native + Expo. iOS and Android from one codebase.
Server + EMR bridge
packages/server · packages/emr-bridgeNode REST API + Python service that connects to 11 EHRs.
At a glance
What it's built with
Every group is a real dependency in the codebase, not a wish list.
Repository structure
A pnpm + Turborepo monorepo. Apps consume shared packages; nothing is duplicated.
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-docsin non-prod.
Multi-tenancy & data
- One PostgreSQL database, two migration tracks: global (one schema) and tenant (one schema per organization).
organization_idis injected into the request context by middleware. Every query is scoped to it.- Subdomain routing —
{org-slug}.mypery.commaps to the org. - Two ORMs: Sequelize for runtime queries, Prisma for
schema.prismaintrospection and type generation.
Authentication & RBAC
One JWT, three roles, enforced at the middleware boundary on the server.
- 1Sign up with phone number — Twilio sends SMS verification.
- 2AWS Cognito issues the JWT. Claims include
user_id,organization_id,role. - 3Server enforces:
jwt-parser-middleware→organization-middleware→role-authorizer. - 4Web validates session via Amplify's
fetchAuthSessionin Next.js middleware; subdomain selects the org. - 5Mobile uses AWS Amplify with AsyncStorage for token persistence.
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 withmypy, containerized separately.
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.
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.