Architecture & Deployment Plan
Cross-region deployment learnings: AWS Japan, Philippines EC2 demo, Thai on-prem air-gapped.
Learnings from deploying to AWS Japan, Philippines (EC2 demo), and planning for Thai on-premise air-gapped hospitals and countryside clinics.
Table of Contents
- System Overview
- Edition System (LITE vs FULL)
- Deployment Topologies
- What We Built Today
- What We Learned
- Hardware Guide by Deployment Type
- Recommended Next Steps
1. System Overview
medOS-ultra monorepo
├── 16 NestJS microservices (Moleculer mesh over NATS)
├── React 19 + Vite frontend (21 packages, 92+ miniapps)
├── MongoDB (write truth) + Supabase/PostgreSQL (read models)
├── Module system (registry + dynamic loading + plugin manager + marketplace)
├── 2 app modes: CLINIC (multi-tenant) / HOSPITAL (single-tenant)
├── 17 locale translations (ja, fil, th, en, etc.)
└── Market packs (region-specific seed data, insurance rates)
Key Existing Infrastructure
| System | File | Status |
|---|---|---|
| App Mode (CLINIC/HOSPITAL) | web/src/config/app-mode.config.ts |
Working — gates features by mode |
| Feature Flags | web/src/utils/featureFlags.ts |
Working — VITE_FEATURE_* env vars |
| Module Registry | web/src/lib/modules/moduleRegistry.ts |
Working — isActive() per module |
| Dynamic Module Loading | web/src/lib/module-federation/ |
Working — manifest-based lazy loading |
| Plugin Manager | web/src/lib/modules/plugin-manager.ts |
Working — runtime .zip install |
| App Store / Marketplace | web/src/lib/appstoreSupabaseClient.ts |
Working — separate Supabase instance |
| Edition Flag | main-flow-editor/config/index.ts |
Partial — only for workflow editor (SELF_HOSTED/CLOUD) |
2. Edition System
Proposed Editions
| Edition | Target | Modules | Backend Services |
|---|---|---|---|
| LITE | Small clinics, countryside, mobile units | OPD + basic IPD | 5-6 core services |
| FULL | Hospitals, nursing homes | Everything | All 13+ services |
LITE Edition Scope
Included:
- Patient registration + queue management
- Screening + consultation
- Prescription + pharmacy dispensing
- Basic billing + receipts
- Appointment scheduling
- Medical records + diagnosis
- Lab orders + imaging orders
- Basic IPD (admission, discharge, ward board)
- Nursing log + vital signs + progress notes
Excluded (FULL only):
- Surgery / operating room / anesthesia
- Blood bank
- Forensics
- Emergency department (advanced)
- ICU / CCU monitoring
- Nursing home (kaigo) workflows
- Clinical decision support (CDS)
- Workflow editor (visual flow builder)
- Advanced analytics / dashboards
- Multi-location management
- FHIR / HL7 interoperability engine
- Advanced RCM (claim submission, denial management)
How to Implement
Use the existing isFeatureEnabled() system + module registry isActive(). No code deletion — modules just don’t activate.
VITE_EDITION=LITE → Only LITE modules register at startup
VITE_EDITION=FULL → All modules register (default)
New file: web/src/config/edition.config.ts — maps edition → module list
Modify: web/src/config/app-mode.config.ts — add getEdition(), wire into feature gates
3. Deployment Topologies
Topology A: Cloud-Connected Client (Browser Only)
Clinic (countryside) Central Server
┌──────────────────┐ ┌──────────────────┐
│ Any device with │── internet ─│ EC2 / VXRail │
│ a browser │ │ Full backend │
│ No install │ │ Shared tenancy │
└──────────────────┘ └──────────────────┘
| Aspect | Detail |
|---|---|
| Client | Browser (laptop, tablet, phone) |
| Server | Centralized (cloud or hospital data center) |
| Edition | LITE or FULL (controlled by build/env var) |
| Internet | Required (always) |
| Cost/clinic | $0 — just needs a browser |
| Offline | No |
| Multi-tenant | Yes — use VITE_APP_MODE=CLINIC with tenant isolation |
| Best for | Connected clinics, mobile health teams, pilot programs |
Deployment: Vercel frontend build with VITE_EDITION=LITE, pointing to central backend. Each clinic gets a tenant ID.
Topology B: Local Server (Offline-Capable)
Clinic (air-gapped or unreliable internet)
┌─────────────────────────────────────┐
│ Mini PC (Intel NUC / Beelink) │
│ Docker Compose (LITE services) │
│ MongoDB + 5 services + frontend │
│ Serves clinic LAN via nginx │
│ Works fully offline │
└─────────────────────────────────────┘
↕ (optional sync when online)
┌──────────────────┐
│ Central Server │
│ (for backup/ │
│ reporting) │
└──────────────────┘
| Aspect | Detail |
|---|---|
| Hardware | Mini PC: 4-8 cores, 8-16GB RAM, 128GB SSD (~$200-500) |
| Services | 5-6 only: gateway, auth, foundation, clinical, financial, (+messaging) |
| Database | Local MongoDB (single node, no replica set needed for LITE) |
| Edition | LITE |
| Internet | Not required — fully offline |
| Cost | ~$200-500 one-time hardware |
| Sync | Future: background sync to central server when internet available |
| Best for | Rural clinics, island clinics, mobile hospitals, disaster response |
Deployment: Air-gapped package (package-airgap.sh) with LITE compose file. USB transfer.
Minimum hardware:
| Component | Spec | Cost |
|---|---|---|
| Beelink SER5 (Ryzen 5, 16GB, 500GB) | 6C/12T, 16GB DDR4, 500GB NVMe | ~$300 |
| Or Intel NUC 12 Pro | 4C/8T, 16GB DDR4, 256GB NVMe | ~$400 |
| Or Raspberry Pi 5 (8GB) + SSD | 4C, 8GB, 256GB USB SSD | ~$150 (tight) |
| UPS (small) | APC BE425M | ~$50 |
Topology C: Hospital On-Premise (Full Stack)
Hospital Server Room
┌─────────────────────────────────────────┐
│ Dell VXRail / PowerEdge / Server │
│ Docker Compose (FULL) or native Node │
│ All 13 services + MongoDB + PostgreSQL │
│ VITE_EDITION=FULL │
│ VITE_APP_MODE=HOSPITAL │
│ Serves hospital LAN │
│ Air-gapped capable │
└─────────────────────────────────────────┘
| Aspect | Detail |
|---|---|
| Hardware | 16+ cores, 32-64GB RAM, 500GB+ SSD |
| Services | All 13 microservices + full infrastructure |
| Edition | FULL |
| Internet | Not required (air-gapped OK) |
| Cost | $9,000-70,000 depending on hardware |
| Best for | Hospitals, nursing homes, medical centers |
Already built and tested — see docs/DEPLOY_ONPREMISE_AIRGAPPED.md.
Topology D: Cloud (AWS)
AWS Region (ap-northeast-1 / ap-southeast-1)
┌──────────────────────────────────────────────┐
│ ECS Fargate → 13 services │
│ DocumentDB → MongoDB │
│ RDS → PostgreSQL │
│ S3 + CloudFront → Frontend │
│ ALB → Path-based routing │
│ Supabase → Read models + realtime │
└──────────────────────────────────────────────┘
| Aspect | Detail |
|---|---|
| Cost | ~$460-650/month (production) or ~$95/month (demo EC2) |
| Edition | FULL |
| Best for | Multi-hospital chains, SaaS offering, demo/staging |
Already built — Terraform modules at infrastructure/terraform/modules/medos-stack/.
4. What We Built Today
Infrastructure Created
| Category | Files | Purpose |
|---|---|---|
| Terraform shared module | 10 .tf files |
VPC, ECS, ALB, S3/CF, RDS, DocumentDB, ECR |
| Japan region config | 3 files | ap-northeast-1 Terraform |
| Philippines region config | 3 files | ap-southeast-1 Terraform |
| Philippines demo (EC2) | 2 files | Single EC2 t3.xlarge → r6i.xlarge |
| Docker Compose (Japan) | 1 file | All 15 services, TZ=Asia/Tokyo |
| Docker Compose (Philippines) | 1 file | All 15 services, TZ=Asia/Manila |
| Docker Compose (on-premise) | 1 file | All-in-one with health checks |
| Market pack: Japan | 6 files | Hospital + nursing home + kaigo rates (JA/EN) |
| Market pack: Philippines | 6 files | Hospital + PhilHealth rates (FIL/EN) |
| Market pack: English | 3 files | International hospital (EN) |
| Market pack: Thailand | 1 file | Manifest (seeds in web/scripts/) |
| Deploy script | 1 file | Multi-region CLI |
| Air-gap packager | 1 file | Build → tar.gz for USB transfer |
| On-premise setup | 3 files | SSL gen, mongo init, setup wizard |
| Nginx on-premise | 1 file | Docker DNS-based proxy config |
| Env files | 8 files | .env.japan/philippines/onpremise/lite per layer |
| Documentation | 4 files | CLAUDE.md, AGENTS.md, DEPLOY_PIPELINE.md, DEPLOY_AIRGAPPED.md |
Live Deployment
| Resource | Detail |
|---|---|
| EC2 Instance | i-0e4d61b97464b5477 (r6i.xlarge, 32GB) |
| Public IP | 3.0.73.73 |
| Region | ap-southeast-1 (Singapore) |
| SSH Key | ever-oma-mongo |
| Services Running | 13 NestJS microservices + MongoDB + NATS + PostgreSQL + IPFS |
| Memory | 4.6GB / 30GB used |
| Cost | ~$95/month |
5. What We Learned
Build Issues Encountered & Fixed
| Issue | Root Cause | Fix |
|---|---|---|
moleculer-db-adapter-mongoose@^0.9.12 not found |
Version gap in npm (0.9.4 → 0.10.1) | Changed to ^0.10.1 in services/interoperability/package.json |
platform-api-schema build fails |
Missing coverage/ module (was deleted or never created) |
Created stub at packages/platform-api-schema/src/administration/coverage/ |
| Docker build 41GB cache | Compose builds 14 identical images with separate caches | Build API image ONCE, reuse for all services (same image, different SCOPE env var) |
yarn install timeout in Docker |
Docker bridge DNS issues | Use --network=host for Docker build |
turbo: not found |
Not installed globally on EC2 | sudo npm install -g turbo@2.5.4 |
bunx: not found |
Bun not installed on EC2 | `curl -fsSL https://bun.sh/install |
| EC2 OOM (16GB) | 13 Node.js services + 4 Docker containers | Upgraded to r6i.xlarge (32GB) |
| SSH timeout | Too many inline commands | Write startup script to file, scp + execute |
| vCPU limit exceeded | AWS default limit is 8 vCPU for standard instances | Used r6i family (different quota bucket) |
| eform Dockerfile broken | References apps/eform/ path that doesn’t exist in medOS-ultra |
Skip eform Docker build, run natively |
| nginx config paths | Compose resolves ../docker/nats relative to compose file |
Use stock nats:2-alpine image instead of custom build |
Key Architecture Insights
-
Single API image, multiple services — All 13 NestJS services use the SAME Docker image. The
SCOPEenv var at runtime determines which service starts. Don’t build 13 separate images. -
Moleculer-runner is the entry point — Services start via
moleculer-runner --config ./dist/moleculer.config.js, NOT vianode dist/moleculer.config.js. -
Build order matters — Must build
platform-*packages first (shared schemas), then services. -
CLINIC vs HOSPITAL is runtime, not build-time — The same binary supports both modes. Switched via env var.
-
Supabase is for read models — The SQL seeds (wards, hospitals, pathology) go to Supabase, NOT to eform PostgreSQL. Insurance rates (kaigo, PhilHealth) go to eform PostgreSQL.
-
Thai seeds are the default — They live in
web/scripts/andweb/supabase/migrations/. Other regions have dedicated market packs ininfrastructure/market-packs/. -
All seeds are additive — Region-prefixed IDs (
jp-ward-*,ph-ward-*,en-ward-*) mean you can load ALL regions into one database.
6. Hardware Guide
For LITE Edition (Small Clinic)
| Option | Spec | Cost | Good For |
|---|---|---|---|
| Beelink SER5 | Ryzen 5 5560U, 16GB, 500GB NVMe | ~$300 | Best value for clinic server |
| Intel NUC 12 | i5-1240P, 16GB, 256GB NVMe | ~$400 | Reliable, well-supported |
| Raspberry Pi 5 | ARM, 8GB, 256GB USB SSD | ~$150 | Cheapest, tight on RAM |
| Any old laptop | i5+, 8GB+, 128GB+ SSD | $0 | Repurposed hardware |
For FULL Edition (Hospital)
| Option | Spec | Cost | Good For |
|---|---|---|---|
| Dell PowerEdge R760 | 2x Xeon, 128GB, 4x SSD | $8-12K | Standalone server |
| Dell VxRail E660 | 2x Xeon Silver, 128GB | $25-35K | VMware hyperconverged |
| Dell VxRail V670 | 2x Xeon Gold, 256GB | $45-65K | 200+ bed hospital |
| AWS EC2 r6i.xlarge | 4 vCPU, 32GB | $95/month | Cloud demo/staging |
For Cloud
| Size | Instance | Cost | Users |
|---|---|---|---|
| Demo | r6i.xlarge | $95/mo | 2-3 devs |
| Small | Full Terraform stack (t3.medium DBs) | $300/mo | <100 users |
| Production | Full Terraform stack (m5.large DBs) | $650/mo | 100-500 users |
7. Recommended Next Steps
Immediate (this week)
- Create
edition.config.ts— Define LITE/FULL module lists - Create
docker-compose-lite.yml— 5-service compose for clinic deployment - Connect Supabase to the Philippines EC2 — load the ward/hospital/pathology seeds
- Start the Vite frontend on the EC2 —
pnpm install && pnpm dev - Test LITE mode — build with
VITE_EDITION=LITE, verify excluded modules don’t appear
Short-term (this month)
- Fix the Docker build pipeline — Single API image build, proper multi-stage caching
- Create GitHub Actions deploy workflow — Auto-deploy to EC2 on push to main
- Create Vercel preview for Philippines — Separate env vars pointing to 3.0.73.73
- Test air-gapped deployment end-to-end — Build package on Mac, transfer to clean Ubuntu VM, verify
Medium-term (next quarter)
- Offline sync for LITE clinics — Background data sync to central server when internet available
- Central dashboard — Admin view showing all connected clinics, their status, last sync time
- Auto-update system — LITE clinics pull update packages from central server
- Billing integration — Connect kaigo rates and PhilHealth rates to the financial module
- CI/CD for air-gapped — Build pipeline that produces versioned
.tar.gzrelease artifacts
Decision Matrix: Which Topology?
Does the clinic have reliable internet?
├── YES → Topology A: Cloud Client (browser only, $0/clinic)
│ Use CLINIC mode + LITE edition on Vercel
│
└── NO → Does the clinic need to work offline?
├── YES → Topology B: Local Server ($200-500 mini PC)
│ Use docker-compose-lite.yml + LITE edition
│
└── SOMETIMES → Both
Local server for daily ops (offline)
+ Cloud sync when connected (future)
Is this a hospital (>50 beds)?
├── YES → Topology C: On-Premise Full ($9K-70K server)
│ Or Topology D: AWS Cloud ($300-650/month)
│ Use HOSPITAL mode + FULL edition
│
└── NO → Use LITE edition (either Topology A or B)