Architecture Overview
Modular microservice architecture for the YourEra telemedicine platform. Each service is independently deployable, testable, and documented.
System Diagram
Eight services with clear boundaries. Arrows show HMAC-authenticated service-to-service calls.
Pipeline Walkthrough
The complete patient journey from intake to shipment, showing which service handles each step.
Patient fills out medical history, selects plan, pays via Stripe. React SPA encrypts data locally (AES-256-GCM) and submits to Intake Gateway.
Intake Gateway validates the HMAC-signed plan token (amount matches Stripe), creates a FHIR Patient in Canvas, and queues an "Rx Review" task for physicians.
Physician Registry assigns the task to all eligible physicians for the patient's state. Multiple doctors see it in their RxQueue. First to claim wins.
Physician opens the case in Admin Portal, reviews H&P, selects dosage, and clicks Approve. The Integration Service forwards the approval to the Orchestrator.
Prescription Orchestrator executes 7 steps in sequence: resolve prescriber via Physician Registry, route pharmacy via Pharmacy Router, submit Rx, charge Stripe payment, notify patient via Notification Service, create shipment via Shipping Service, schedule refills.
For GMP (Galleria): Shipping Service auto-generates a FedEx label and adds the Rx to the print queue. For Boothwyn: the pharmacy ships independently and sends a webhook to the Pharmacy Router, which forwards the tracking number to the Integration Service via an HMAC-signed callback.
Notification Service sends email + SMS with tracking info. Channels are configurable per notification type via the Admin Portal.
Service Catalog
Inter-Service Communication
All service-to-service calls use HMAC-SHA256 authentication (same pattern as Pharmacy Router and Physician Registry).
// Request headers
X-API-Key: <identifies the caller>
X-Timestamp: <ISO 8601, within 5 min>
X-Signature: HMAC-SHA256("<timestamp>.<jsonBody>", apiSecret)
Call Graph
Database Topology
Each service owns its own database in the shared RDS instance. No cross-service DB access — services communicate only via APIs.
| Service | Database | Key Tables |
|---|---|---|
| Intake Gateway | intake_gateway | intake_plans, intake_submissions, intake_retry_queue, intake_verification_codes |
| Orchestrator | orchestrator | orchestration_runs, payment_holds, refill_schedules, patient_mappings, sync_logs, medication_configs |
| Physician Registry | physician_registry | physicians, physician_licenses, physician_routing, prescription_assignments, registry_api_keys |
| Pharmacy Router | pharmacy_router | router_submissions, router_pharmacy_config, router_api_keys |
| Notification Svc | notification_service | notification_templates, notification_channel_config, notification_log |
| Shipping Service | shipping_service | shipments, shipping_users, tracking_cache, print_queue |
| Integration Svc | hisera (existing) | admin_otp_codes, telehealth_appointments (shrinks over time) |
| GuideGLP | guideglp | guide_physicians, guide_orders, guide_refill_schedules, guide_invoice_periods |
Multi-Physician Claim Model
Enables a pay-per-visit model where multiple physicians see the same patient. First to claim gets the case.
New intake arrives. Physician Registry finds all eligible physicians for the patient's state and creates a prescription_assignments row with status=open. Each eligible physician gets an entry in the junction table.
Registry calls Notification Service to alert each physician (email + Slack). Physicians see the task in their RxQueue.
Physician clicks "Review". Admin Portal calls POST /route/claim. Registry executes atomic SQL: UPDATE…WHERE status='open'. If already claimed by another physician, returns 409 Conflict. Race-safe via PostgreSQL row-level locking.
Physician approves/denies. Assignment status transitions to completed. The physician is credited for the visit.
-- Atomic claim (exactly one winner in a race)
UPDATE prescription_assignments
SET status = 'claimed',
claimed_by = $physicianId,
claimed_at = NOW()
WHERE id = $assignmentId
AND status = 'open';
Notification System
All notifications flow through the Notification Service. Templates and channel toggles are stored in the database and editable via the Admin Portal.
Channel Matrix
| Notification Type | SMS | HubSpot | Slack | |
|---|---|---|---|---|
| prescription_approved | ON | ON | ON | — |
| prescription_denied | ON | ON | — | — |
| shipment_update | ON | ON | ON | — |
| payment_charged | ON | ON | ON | — |
| payment_failed | ON | ON | — | — |
| provider_message | ON | ON | ON | — |
| refill_reminder | ON | ON | — | — |
| welcome | ON | — | ON | — |
| physician_new_task | ON | — | — | ON |
| admin_alert | — | — | — | ON |
Each toggle is stored in notification_channel_config and editable via PUT /notify/channels/:type. The Admin Portal provides a UI with toggle switches.
Galleria Print Queue
Part of the Shipping Service. When the Orchestrator creates a GMP shipment, a print queue entry is auto-created. Galleria pharmacy staff see it in the Shipping Dashboard.
-- Print queue lifecycle
pending → printing → printed → (flows into pack/ship)
→ error → (retry)
| Endpoint | Method | Purpose |
|---|---|---|
/shipping/print-queue | GET | List prescriptions pending print |
/shipping/print-queue/:id/printed | POST | Mark a prescription as printed |
Secure Intake Validation
Prevents client-side price tampering. The Intake Gateway signs plan selections with HMAC, then verifies the signature matches the Stripe amount at payment time.
// 1. Server generates plan token when client selects a plan
const payload = { planId, amountCents, intakeType, ts: Date.now() };
const data = base64url(JSON.stringify(payload));
const sig = HMAC-SHA256(data, PLAN_SIGNING_SECRET);
const planToken = data + '.' + sig;
// 2. Client sends planToken with Stripe PaymentIntent
// 3. Server verifies on payment submission
const [data, sig] = planToken.split('.');
const expected = HMAC-SHA256(data, PLAN_SIGNING_SECRET);
if (sig !== expected) throw 'Tampered plan token';
if (payload.amountCents !== stripeAmount) throw 'Amount mismatch';
if (Date.now() - payload.ts > 15 * 60 * 1000) throw 'Token expired';
Migration Phases
Services are extracted in dependency order — leaf services first, then services that depend on them.
lib/notifications/. Deploy, seed templates from hardcoded HTML. Update monolith to call the service. Add Admin Portal "Notifications" page.lib/fedex/ and shipment routes. Migrate shipments, shipping_users, tracking_cache tables. Add Galleria print queue. Update Shipping Dashboard.orchestrator.ts and supporting modules. Migrate payment_holds, refill_schedules, patient_mappings, sync_logs. Move refill cron here.ALB Routing
All services share a single domain (api.yourera.com) with path-based routing on the ALB.
| Priority | Path Pattern | Service |
|---|---|---|
| 1 | /rx/* | Pharmacy Router |
| 2 | /physicians/*, /route/* | Physician Registry |
| 3 | /orchestrator/* | Prescription Orchestrator |
| 4 | /notify/* | Notification Service |
| 5 | /shipping/* | Shipping Service |
| 6 | /intake/* | Intake Gateway |
| 50 | /* (default) | Integration Service |
Testing Strategy
Each service follows the established pattern (vitest, same as Pharmacy Router):
- Unit tests — Pure logic: routing, validation, HMAC, template rendering
- Service tests — Mocked HTTP dependencies (mock Physician Registry responses, etc.)
- Contract tests — Each service exports TypeScript interfaces for request/response shapes. Callers import and validate.