Architecture Overview

Canvas-first microservice architecture. Canvas FHIR is the medical-record database of record; every service we build is a single-purpose sidecar around it. One Canvas instance serves YourEra DTC and every GuideGLP B2B client via FHIR-native multi-tenancy.

The Principle

Canvas FHIR is the medical-record database. Every clinical fact lives in Canvas. Our services are sidecars around it.

For everything else — payments, shipping logistics, tenant branding, notification delivery, schedulers, compliance archives — we build lean single-purpose sidecar services that hold the minimum state needed to do their job and reference Canvas for clinical truth. One Canvas instance, many Organizations: YourEra DTC is a FHIR Organization; each GuideGLP client (Biologics, LCMC, Sleep Corner, etc.) is also a FHIR Organization. Every Patient has a managingOrganization pointing to one of them.

The decision rule when designing anything new:

Would a HIPAA auditor expect this data in an EMR? Then Canvas owns it.
Would a Stripe auditor, FedEx tracking system, or CMS-style operational dashboard expect this data? Then a sidecar owns it.

No caching Canvas data "just in case." No storing clinical fields in sidecars. No cross-database joins. Read canvas-first-principles for the full doctrine.

System Diagram

Canvas-first service mesh. Every service is a sidecar around Canvas FHIR. Portals (patient + admin) read/write Canvas directly via the @yourera/canvas-client shared library; sidecars either write Canvas at intake completion (intake-gateway), via the saga (prescription-orchestrator writing MedicationDispense + Task), or never (most). Arrows show the primary data flow; async event traffic flows through the central event bus (SQS + EventBridge) with outbox delivery guarantees.

Patient Portal {slug}.portal.yourera.com + custom domains Next.js · Clerk auth · white-label Admin Portal admin.hisera.com Next.js · tenant-scoped · RBAC Intake Sites per-org intake hosts (GLP-1 · NAD+ · Microdose · B2B) CANVAS FHIR — shared across ALL organizations Patient · Observation · Condition · AllergyIntolerance · MedicationRequest MedicationDispense · Task · Communication · Consent · DocumentReference Organization (multi-tenancy) · Practitioner · PractitionerRole 5 SDK plugins, all org-aware (prescription_queue, portal_invite, etc.) Intake Gateway intake_sessions Writes Canvas atomically at intake completion Webhook Receiver Single public endpoint for Canvas webhooks Fan-out via event bus writes webhooks event bus — SQS + EventBridge (outbox pattern, at-least-once) Organization Service orgs, branding, custom domains, admin users, physician memberships Patient Service SLIMMED · 2 tables patient_directory + intake_snapshots Physician Registry state licenses · NPI routing rules Notification Service — EXPANDED delivery (SES + Twilio) · contacts (leads + patients) campaigns (drips + scheduled) · preferences replaces HubSpot entirely Refill Scheduler materialized view of refills due, fed by Canvas webhooks · fires refill.due Prescription Orchestrator SLIMMED · saga / state machine our SureScripts substitute Pharmacy Router GMP (primary) Strive (fallback) live routing per saga Commerce Service orders, subscriptions, catalog, billing plans, coupons Payment Service Stripe bridge (extracted from canvas-pioneer) customers · PI · subs · disputes Shipping Service FedEx 2Day Express label + tracking Galleria print queue @yourera/canvas-client (library) Shared Canvas OAuth + FHIR HTTP Used by every service that touches Canvas Galleria (GMP) PioneerRx backend PRIMARY pharmacy Strive fallback pharmacy (when GMP unlicensed) Stripe PCI + billing FedEx 2Day Express SES + Twilio per-org verified senders Clerk patient + admin auth pharmacy dispense callbacks → pharmacy-router → writes Canvas MedicationDispense Existing New Enhanced scope Slimmed / library External EMR

For the architectural doctrine behind this diagram (why Canvas-first, what can and cannot live in a sidecar, what we explicitly reject) see canvas-first-principles.

Pipeline Walkthrough

The full patient journey from intake to delivered Rx under the Canvas-first architecture.

1
Patient Starts Intake

Patient reaches their org's intake site (e.g., intake.biologics.com/start-online-visit/glp-1). Intake Gateway resolves the org via Organization Service, creates an intake_sessions row, and emits contact.captured once email is collected. Notification Service creates a contacts row — the lead now exists.

2
Intake Flow + Atomic Canvas Write

Patient completes all intake steps. On final submit, Intake Gateway atomically writes Canvas resources via canvas-client: Patient (with managingOrganization set), Observation (vitals), Condition (history), AllergyIntolerance, MedicationStatement, QuestionnaireResponse, Consent, and DocumentReference (ID scans). In the same logical transaction, Patient Service writes the patient_directory row + the INSERT-only intake_snapshots compliance record.

3
Commerce + Payment at Intake Checkout

Commerce Service creates a subscription + order #1 based on the plan selected. Payment Service creates a Stripe SetupIntent (payment method collection) and then a PaymentIntent for the initial charge — quarterly and 6-month plans are charged fully upfront; monthly plans charge per fill. On successful capture, order → paid, subscription → active.

4
Provider Review + Canvas MedicationRequest Activation

Canvas Task is routed to eligible providers (via Physician Registry + the Canvas prescription_queue plugin, now org-aware). Provider reviews H&P in Canvas, approves, and signs the prescription. Canvas emits MedicationRequest.status = active. If the provider rejects with a free-form note, the MR stays draft or cancelled — no saga runs, no charge reversed until admin triggers refund.

5
Orchestrator Picks Up the Saga

Webhook Receiver ingests Canvas's webhook and emits canvas.medication_request.activated on the internal event bus. Prescription Orchestrator creates an rx_fill_sagas row and runs the state machine: preflight (allergy, contraindication, org status) → payment (skipped if order is prepaid per plan cadence) → pharmacy submission.

6
Pharmacy Routing (Live Per Saga)

Orchestrator asks Pharmacy Router for a route via getRoute(org, state, medication). Router reads the live pharmacy_routes table: Galleria (GMP) if licensed in the patient's state, else Strive. Routing is re-evaluated every fill — a patient who went to Strive for fill #1 automatically routes to GMP for fill #2 if GMP adds a license in between. No per-patient routing pin. Pharmacy dispenses; pharmacy-router writes Canvas MedicationDispense on callback.

7
Shipping + Patient Notification

Shipping Service generates a FedEx 2Day Express label (fetching patient address live from Canvas — never cached) and delivers it to the pharmacy via signed S3 URL. The pharmacy manages cold-chain packaging (their domain, not ours). As carrier events flow in, Notification Service sends templated updates to the patient (shipped, out-for-delivery, delivered) using the org's verified sender identity.

8
Refill Cycle — Event-Driven, Not Cron

Refill Scheduler maintains a lean materialized view of every active MedicationRequest, fed by Canvas webhooks. When next_fill_due_at ≤ now, it emits refill.due. The orchestrator picks up, creates a new saga, and runs the full pipeline again. Prepaid-plan refills skip the charge stage (money already collected); pay-as-you-go refills generate a new PaymentIntent. A nightly reconcile worker diffs the view against Canvas to catch any webhook drops.

Service Catalog

Organization Service
Tenants, branding, portal config, custom domains (DNS+TLS lifecycle), admin users, admin memberships, physician × organization memberships. The backbone of multi-tenancy.
New Express 5
Patient Service
Directory service only. patient_directory maps MRN ↔ canvas_patient_id ↔ stripe_customer_id ↔ clerk_id ↔ organization_id. intake_snapshots is INSERT-only compliance. Two tables. No clinical data.
Slimmed Express 5
Intake Gateway
Captures intake, writes Canvas resources atomically at completion, emits lead + conversion events. Holds intake_sessions for resume + lead capture; never a staging blob.
Enhanced Express 5
Webhook Receiver
Single public endpoint for Canvas webhooks. Normalizes + fans out to internal event bus. Keeps public-endpoint surface area and Canvas-auth state isolated from every other service.
New Express 5
Prescription Orchestrator
Saga / state-machine service. Our SureScripts substitute. Coordinates preflight → payment → pharmacy → shipping → notification per fill. Holds only saga state; no patient, payment, or pharmacy data.
Slimmed Express 5
Refill Scheduler
Event-driven materialized view of refills due, fed by Canvas MedicationRequest + MedicationDispense webhooks. Fires refill.due; never writes Canvas directly. Nightly safety-net reconciler.
New Express 5
Pharmacy Router
Sole owner of pharmacy HTTP integration. Galleria (GMP, primary) + Strive (fallback); Boothwyn dropped. Live routing re-evaluated per fill saga against pharmacy_routes. Writes Canvas MedicationDispense on dispense callback.
Enhanced Express 5
Physician Registry
Physician directory, state licenses, NPI. Consulted by pharmacy-router + Canvas plugins for state-based Rx routing. Now integrates with physician_org_memberships in organization-service.
Exists Enhanced
Payment Service
Stripe bridge. Customers, payment methods, SetupIntents, PaymentIntents, subscriptions, refunds, disputes. Idempotent everywhere. Zero clinical data.
New Express 5
Commerce Service
Orders as correlation keys joining Stripe PI + Canvas MR + shipment + coupon + saga. SKUs, billing plans (monthly / quarterly-prepaid / 6-month-prepaid), coupons, business-level subscriptions.
New Express 5
Shipping Service
FedEx 2Day Express label + tracking. Patient address fetched live from Canvas at label time — never cached. Pharmacy-ships mode (Mode B) also supported for tracking-only. Galleria print queue for pharmacy staff.
Enhanced Express 5
Notification Service
Delivery (SES + Twilio) + contacts (leads & patients) + campaigns (drips + scheduled + blasts) + preferences (HIPAA-aware). Replaces HubSpot entirely. Per-org verified sender identity + template overrides.
Enhanced Express 5
@yourera/canvas-client
Shared TypeScript library. Canvas OAuth + FHIR HTTP helpers + typed resource wrappers. Every service that talks to Canvas uses this library; no service reinvents Canvas auth.
New library
What is NOT a service in this architecture Stripe/Canvas clients are libraries, not services. A separate auth service is unnecessary — Clerk handles patient + YourEra staff; OTP+JWT handles B2B org admins; each service verifies tokens on its own endpoints. There is no Integration Service anymore — its responsibilities redistributed across organization-service, payment-service, notification-service, canvas-client, and webhook-receiver.

Inter-Service Communication

Two communication styles, picked per use case:

  1. Async events (primary). Outbox pattern from each producer; SQS/EventBridge delivers at-least-once. Consumers must be idempotent. Used for everything that doesn't need a synchronous response.
  2. Direct HTTP (fallback). Service-to-service JWT signed with a shared secret per service pair. Used when the caller genuinely needs the response now (e.g., admin-portal calling commerce to create a refund).

Event Naming Convention

{domain}.{entity}.{past-tense}

Examples: intake.completed, patient.created, payment.intent_succeeded, rx.fill_completed, refill.due, pharmacy.dispense_confirmed, shipping.delivered.

Call Graph (HTTP, when sync is required)

Intake Gateway Patient Service POST /patients (atomic with Canvas write)
Intake Gateway Patient Service POST /patients/email-lookup (dup detection)
Intake Gateway Organization Service resolve host → org
Orchestrator Pharmacy Router POST /submissions
Orchestrator Payment Service POST /payments/charges
Orchestrator Shipping Service POST /shipments
Admin Portal Commerce Service POST /orders/:id/refund
Admin Portal Orchestrator POST /sagas/:id/unblock
Patient Portal canvas-client Clinical reads (live)

Async Event Examples

Intake Gateway event bus intake.abandoned → notification enrolls abandoned drip
Patient Service event bus patient.created → payment, commerce, notification
Canvas (via WebhookReceiver) event bus canvas.medication_request.activated → orchestrator
Refill Scheduler event bus refill.due → orchestrator creates new saga
Pharmacy Router event bus pharmacy.dispense_confirmed → orchestrator advances
Shipping Service event bus shipping.delivered → orchestrator completes + notification sends

Database Topology

Each service owns its own Postgres database. No cross-service DB access; services communicate only via APIs and events. No DB-level foreign keys cross service boundaries — UUIDs only, app-enforced.

ServiceDatabaseKey Tables
Organizationorganization_serviceorganizations, organization_branding, organization_portal_config, organization_notification_config, organization_custom_domains, admin_users, admin_org_memberships, physician_org_memberships
Patientpatient_servicepatient_directory, intake_snapshots
Intake Gatewayintake_gatewayintake_sessions
Orchestratorprescription_orchestratorrx_fill_sagas, saga_events
Refill Schedulerrefill_schedulerrefill_schedule_views, scheduler_events
Pharmacy Routerpharmacy_routerpharmacies, pharmacy_routes, pharmacy_submissions, pharmacy_callbacks
Physician Registryphysician_registryphysicians, physician_licenses, physician_routing
Paymentpayment_servicecustomers, payment_methods, setup_intents, payment_intents, charges, refunds, stripe_subscriptions, disputes, stripe_webhook_events
Commercecommerce_serviceproducts, skus, billing_plans, coupons, coupon_redemptions, subscriptions, orders, order_items
Shippingshipping_serviceshipments, shipment_items, tracking_events
Notificationnotification_servicecontacts, campaigns, campaign_enrollments, messaging_preferences, notification_log
Webhook Receiverwebhook_receiverwebhook_events (normalized, fan-out state)

Every service also has outbox and audit_events tables for event reliability and action audit, per the Canvas-first doctrine.

Multi-Tenancy

One Canvas instance serves every Organization — YourEra DTC and every GuideGLP client. FHIR-native multi-tenancy via Patient.managingOrganization. Tenant isolation is enforced in our service layer, not by Canvas. GuideGLP clients get perceived isolation — white-labeled portals, tenant-scoped admin views, their own branded notifications — but share infrastructure.

What Each GuideGLP Client Gets

What We Do NOT Promise

Single-Org-For-Life

A patient belongs to exactly one organization for their entire lifetime with us. A person who is both a DTC customer and a Biologics employee does not get modeled as two records — they enter through one funnel. organization_id is set once at patient_directory creation and is immutable through normal flows. Superadmin corrective reassignment exists as a rare fix-it tool only.

Notification System

All outbound patient comms flow through the Notification Service. The service expanded significantly under Canvas-first: beyond the original delivery-only scope, it now owns a lightweight CRM layer (contacts + campaigns + preferences) that replaces HubSpot entirely.

Category Model (HIPAA-Aware)

Every notification falls into one of three categories. Opt-out rules are category-specific.

CategoryExamplesv1 Opt-Out Behavior
transactional order confirmations, password reset, 2FA codes, payment receipts Cannot opt out. Legally / service-essential.
clinical provider messages, Rx status, refill reminders, appointment reminders, preflight warnings Cannot opt out at v1. Future: per-template granularity via a companion override table.
marketing drip campaigns, weekly tips, educational content, re-engagement Freely opt-out per channel. One-click unsubscribe on every message.

Campaign Types at v1

Open/click tracking, A/B testing, inbound reply threading, and multi-language templates are explicitly deferred; schema is extensible.

Part of Shipping Service. When the orchestrator creates a Galleria (GMP) shipment, a print queue entry is auto-created; Galleria pharmacy staff see it in the Shipping Dashboard and print the label when packaging.

-- Print queue lifecycle
pending    printing    printed    (flows into pack/ship)
                         error      (retry)
EndpointMethodPurpose
/shipping/print-queueGETList prescriptions pending print
/shipping/print-queue/:id/printedPOSTMark a prescription as printed

Secure Intake Validation

Prevents client-side price tampering. The Intake Gateway issues an HMAC-signed plan token at plan selection; the signature is verified when the Stripe PaymentIntent is created. Coupled with Stripe Elements (card data never touches our server) and per-org verified senders, PCI + PHI scope stays tight.

// 1. Server generates plan token when client selects a plan
const payload = { planId, amountCents, intakeType, organizationId, 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 request

// 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 (payload.organizationId !== resolvedOrg) throw 'Org mismatch';
if (Date.now() - payload.ts > 15 * 60 * 1000) throw 'Token expired';

Refactor Sequence

Under Canvas-first, services are built/retired in dependency order. Foundational services first, then the ones that depend on them. See canvas-first-principles §6 for the full rationale.

1
Organization Service
Provision DB + core entities. Bootstrap YourEra DTC + existing GuideGLP clients as organizations rows with Canvas Organization mirrors.
Dependencies: None (foundational)
2
@yourera/canvas-client (shared library)
Extract Canvas OAuth + FHIR HTTP helpers from canvas-pioneer into a package. Every downstream service depends on this.
Dependencies: None (library)
3
Patient Service (slim)
Rebuild from v0 monolith to two tables: patient_directory + intake_snapshots. Migrate existing patient records; drop clinical columns. Add /patients/email-lookup endpoint.
Dependencies: Organization Service (Phase 1), canvas-client (Phase 2)
4
Webhook Receiver
Stand up single public Canvas webhook endpoint. Signature verification. Fan-out via SQS.
Dependencies: canvas-client (Phase 2)
5
Intake Gateway (Canvas-atomic)
Rewrite intake completion to atomically write Canvas resources + patient-service rows. Add intake_sessions persistent table + abandonment sweeper + lead event emission.
Dependencies: Phases 1–4
6
Canvas Plugins (org-aware)
Update prescription_queue, portal_invite, and other SDK plugins to respect managingOrganization for routing + branding. Use Canvas Plugin Assistant (CPA) for development.
Dependencies: Organization Service (Phase 1)
7
Refill Scheduler
Build materialized-view service. Webhook ingest + cadence worker + safety-net reconciler. Emits refill.due; writes no Canvas resources.
Dependencies: Phases 2, 4
8
Pharmacy Router (GMP + Strive)
Remove Boothwyn integration. Build GMP driver (PioneerRx backend); build Strive driver. Seed pharmacy_routes with GMP state-license matrix. Live routing re-evaluation per saga.
Dependencies: Phase 2
9
Prescription Orchestrator (slim saga)
Strip canvas-pioneer-era scope (auth, records, Stripe, pharmacy HTTP, email). Keep only saga state + state machine coordinating payment → pharmacy → shipping → notification. Preflight implementation.
Dependencies: Phases 2, 7, 8
10
Shipping Service (Canvas-address)
Drop cached patient demographics. Fetch address from Canvas live at label time. Default to FedEx 2Day Express. Pharmacy-ships (Mode B) for tracking-only shipments where pharmacy labels directly.
Dependencies: Phases 2, 9
11
Notification Service (expanded)
Expand from delivery-only to include contacts, campaigns, campaign_enrollments, messaging_preferences. Per-org verified senders (SES + Twilio). HIPAA-aware category model.
Dependencies: Phases 1, 3, 4
12
Payment Service (extract from canvas-pioneer)
New service. Stripe customers, PaymentIntents, subscriptions, refunds, disputes. Idempotent everywhere. Zero clinical data; statement descriptors generic per org.
Dependencies: Phases 1, 3
13
Commerce Service
New service. Orders, subscriptions (business-level), catalog (SKUs, billing plans), coupons. Prepaid bundled plans (quarterly, 6-month) charged upfront; pay-as-you-go monthly per fill.
Dependencies: Phases 3, 9, 12
14
Canvas-pioneer-integration-service retirement
Delete the service. Every responsibility has been redistributed; no code remains to port.
Dependencies: Phases 2, 3, 7, 8, 9, 11, 12
15
Admin Portal (tenant-scoped + RBAC)
Rebuild with role (superadmin, org_admin, org_user, support) + memberships resolution. Superadmin impersonate-org mode. Saga operations console. Campaign authoring. Full audit logging.
Dependencies: Phases 1–13
16
Patient Portal (white-label)
Rebuild for hostname → org resolution, custom domains, per-org theming. Clerk auth with org-scoped sessions. Live canvas-client reads; stateless.
Dependencies: Phases 1–13
17
GuideGLP Migration
Retire the guide-glp standalone service + DB. Migrate each GuideGLP client's patients into Canvas as Patient.managingOrganization = {their_canvas_org_id}. Their orders/subs now flow through shared services.
Dependencies: All of Phases 1–16

What Retires

Under Canvas-first, substantial prior-architecture artifacts go away entirely.

Retired ArtifactReasonReplacement
canvas-pioneer-integration-serviceResponsibilities redistributed.canvas-client library + webhook-receiver + specific sidecars.
hubspot-sync-service (never built)HubSpot retired from architecture.notification-service (expanded: contacts + campaigns).
guide-glp (standalone service + DB)GuideGLP patients become Canvas-managed.Shared services with Organization.kind='b2b'.
iOS app + yourera-ios-app repoAlready deprecated.Web-only.
v0 patient-service monolith (~30 tables)Clinical data moved to Canvas.Slim patient-service (2 tables).
Boothwyn pharmacy integrationDropped as a partner.GMP primary, Strive fallback.
patientRecords / intakeData JSONB tablesSee redistribution map.Canvas Patient + patient-service.intake_snapshots.
HubSpot webhooks + CRM syncNot used, never built pipeline.notification-service events.
Daily refill cron (Lambda/EventBridge)Coarse cadence, gaps.Event-driven refill-scheduler + per-minute worker + nightly reconcile.

Testing Strategy

Each service is tested at four tiers:

Verification after each phase All existing tests still pass (no regressions). New service passes independently. End-to-end pipeline works against a test Canvas instance. Tenant scoping verified: cross-org queries return empty for non-superadmin callers. Docs page renders on Vercel. Health check returns 200 on the new ALB path.