HMS Seeder — Idempotent Data Seeding
Audiences: developer, internal
ddx-seeder-tsis a TypeScript CLI tool that populates a Dudoxx HMS environment through REST API calls alone — no direct database, FHIR, or MinIO access — using the SMART idempotency pattern so every phase can be re-run safely without creating duplicates.
Business Purpose
Every new environment (dev, staging, demo, CI) needs a consistent, realistic dataset before clinical workflows can be demonstrated or tested. Manual bootstrapping is error-prone and slow. The seeder provides: (1) a 19-phase pipeline that bootstraps a complete platform from zero to a fully-populated clinic with patients, practitioners, appointments, lab results, and AI templates in under 15 minutes; (2) an idempotency guarantee — re-running any phase is always a no-op for unchanged data and an update for changed data; (3) a state file on disk so a failed seed mid-phase resumes from where it stopped, not from the beginning. This capability is critical for demo environments (Mesalvo, partner POCs) that need to be torn down and rebuilt quickly.
Audiences
- Investor: Demo environments can be provisioned end-to-end in ~15 minutes for sales engagements. Clinical narrative data (ACME Hamburg) is rich enough to demonstrate TUCAN AI, document generation, and FHIR integration in live demos.
- Clinical buyer (doctor/nurse/receptionist): Invisible at runtime — the seeder is a dev/ops tool. Its output is the realistic test data clinical staff see in demo environments.
- Developer/partner: All seeding goes through REST API calls to
ddx-apiathttp://localhost:6100/api/v1. Authentication usesX-API-Key(GATEWAY_API_KEY_SEEDER) +X-Clinic-ID(clinic slug). No direct DB/FHIR/MinIO access. All entities use the SMART pattern. - Internal (ops/support):
pnpm seed:platformbootstraps a fresh environment end-to-end.pnpm seed:org:acme-hamburgpopulates the ACME Hamburg demo clinic with full clinical data. State file at.ddx-seeder-state.jsontracks what has been seeded.
Architecture
The seeder is a Commander.js CLI (ddx-seeder-ts/src/index.ts) with 24 seeder classes, each covering one phase. All seeders extend BaseSeeder which provides the SMART upsert pattern, state management, spinner UI, and dry-run support.
SMART Pattern (mandatory on every seeder):
1. CHECK → Does entity exist? (GET by email / slug / name via API) 2. FETCH → If yes, get existing entity data 3. READ → Compare existing vs. desired state 4. UPDATE → PATCH if different 5. LINK → Record IDs in .ddx-seeder-state.json for downstream phases
Result actions: created | updated | unchanged | failed
Phase pipeline (recommended order):
| # | Phase | Seeder class | What it creates |
|---|---|---|---|
| 0 | init | InitSeeder | System roles + RBAC permission catalog + role-permission mappings + platform settings + feature flags |
| 1 | essentials | EssentialsSeeder | Organizations (tenants via POST /bootstrap/tenants) + super admins + clinic admins |
| 2 | vaccine-schedules | VaccineSchedulesSeeder | Vaccine schedule definitions (platform-scoped, no clinic required) |
| 3 | medication-catalog | MedicationCatalogSeeder | ~1000+ medication catalog entries |
| 4 | assessment | AssessmentSeeder | Assessment form definitions (Zod-validated against form schema) |
| 5 | document-templates | DocumentTemplatesSeeder | Document generation templates + section definitions |
| 6 | document-summarization-templates | DocumentSummarizationTemplatesSeeder | AI summarization prompt templates |
| 7 | treatment-plan-templates | TreatmentPlanTemplatesSeeder | Treatment plan template library |
| 8 | help-cms | HelpCmsSeeder | In-app help content (~5 seconds) |
| 9 | flowagent | FlowAgentSeeder | FlowAgent scenario definitions |
| 10 | diagnosis-engine | DiagnosisEngineSeeder | Diagnosis engine rules (1032 rows, ~8 minutes — the longest phase) |
| 11 | staff | StaffSeeder | Doctors, nurses, receptionists (FHIR Practitioner created on role assignment) |
| 12 | demo | DemoSeeder | Demo patients + login helper accounts |
| 13 | clinical | ClinicalSeeder | Medications, prescriptions (Faker-generated) |
| 14 | uploads | UploadsSeeder | Patient attachments → MinIO via POST /patients/:id/attachments |
| 15 | clinical-history | ClinicalHistorySeeder | Curated clinical history from data/08-clinical-history/*.json |
| 16 | visit-transcriptions | VisitTranscriptionsSeeder | Visit audio transcription data |
| 17 | lab-data | LabDataSeeder | Lab result data |
| (org-level) | acme-clinical-full | AcmeClinicalFullSeeder | Full ACME Hamburg clinical dataset (13 patients + rich history) |
| (org-level) | acme-enrich | AcmeEnrichSeeder | ACME data enrichment pass |
| (org-level) | acme-labs | AcmeLabsSeeder | ACME lab result set |
| (org-level) | acme-org-settings | AcmeOrgSettingsSeeder | ACME org-specific settings |
State management (ddx-seeder-ts/src/utils/state.ts):
- State is serialized to
.ddx-seeder-state.json(path fromSTATE_FILEenv var). - On load, the file is parsed and validated through
SeederStateSchema(Zod). Invalid or missing files start fresh. - Each phase records
completedPhases[]andentities[]with their API-returned IDs. - Downstream phases look up IDs from state (e.g.
StaffSeederlooks uporganizationIdwritten byEssentialsSeeder).
Tech Stack & Choices
| Concern | Choice | Rationale |
|---|---|---|
| CLI framework | Commander.js | Standard Node.js CLI library; supports sub-commands per phase |
| HTTP client | Axios | Consistent with ddx-seeder-ts (not NestJS HttpService — seeder runs standalone) |
| Data validation | Zod | SeederStateSchema, RolesFileSchema, OrganizationsFileSchema validate JSON data files before seeding |
| Data generation | @faker-js/faker | Faker-generated clinical data (prescriptions, vitals) for non-ACME demo patients |
| Terminal output | chalk + ora + cli-table3 | Rich spinner + table output for phase progress |
| State persistence | JSON file (.ddx-seeder-state.json) | Crash-safe; survives partial runs; Zod-validated on load |
| Module format | ES Modules ("type": "module") | Modern Node.js; imports use .js extension per ESM rules |
| Runtime | tsx for dev, compiled JS for prod | TypeScript 5.x with strict mode |
Data Flow
Full platform bootstrap:
cd ddx-seeder-ts pnpm seed:platform # Runs phases: init → vaccine-schedules → essentials → medication-catalog # → assessment → document-templates → document-summarization-templates # → treatment-plan-templates → help-cms → flowagent → diagnosis-engine pnpm seed:org:acme-hamburg # Runs: staff → demo → uploads → clinical-history → acme-clinical-full # → acme-enrich → acme-labs → acme-org-settings
SMART upsert detail (roles, from InitSeeder):
InitSeeder.seed()readsdata/PLATFORM-DATA/roles.json.RolesFileSchema.parse()validates the JSON (Zod).- For each role:
api.smartUpsertRole(role)→GET /roles?name={role.name}(CHECK). - If exists: compare fields →
PATCH /roles/:idif different (UPDATE). - If missing:
POST /roles(CREATE). processSmartResult()incrementscreated/updated/unchanged/failedcounters.- After all roles:
api.seedRbacPermissions()→POST /super-admin/permissions/seed(idempotent bulk endpoint).
Organization creation (EssentialsSeeder):
- Uses
POST /bootstrap/tenants(NOTPOST /organizations) — this triggers the full tenant provisioner pipeline (FHIR partition + MinIO buckets). See Tenant Provisioner.
Implicated Code
ddx-seeder-ts/src/seeders/init.ts:14—InitSeeder; SMART role upsert loop at:43; RBAC permissions seed at:60–80;PHASE_ORDER=0ddx-seeder-ts/src/seeders/essentials.ts:18—EssentialsSeeder; organization creation at:44(readsdata/00-init/organizations.json); super-admin seeding; clinic-admin seeding;PHASE_ORDER=1ddx-seeder-ts/src/utils/state.ts:18—loadState();SeederStateSchemaZod validation at:32; crash-safe state persistence;completedPhases[]+entities[]trackingddx-seeder-ts/src/seeders/base.ts—BaseSeeder; SMART pattern methods (processSmartResult()), spinner helpers, dry-run support; extended by all 24 seeder classesddx-seeder-ts/src/seeders/index.ts:1— seeder registry; exports all 24 seeder classesddx-seeder-ts/src/seeders/diagnosis-engine.ts—DiagnosisEngineSeeder; longest phase (~8 min, 1032 rows across 9 substeps)ddx-seeder-ts/src/types/index.ts—SeederStateSchema,RolesFileSchema,OrganizationsFileSchema,SeederResulttype definitionsddx-seeder-ts/src/utils/config.ts— env config (API_BASE_URL,API_KEY,STATE_FILE,DRY_RUN,CONTINUE_ON_ERROR)
Operational Notes
- API must be running: The seeder calls
GET /healthfirst. Ifddx-apiis not reachable atAPI_BASE_URL(defaulthttp://localhost:6100/api/v1), all phases abort. - Phase ordering matters:
initmust complete beforeessentials(roles must exist before admins), andessentialsbeforestaff(org IDs must be in state). Running phases out of order causesLINKstep failures. CONTINUE_ON_ERROR=true(default): A failed entity does not abort the phase — it incrementsresult.failedand continues. Set tofalsein CI to make failures explicit.- Dry-run support:
DRY_RUN=true(or--dry-runflag) logs what would be created/updated without making any API calls. Useful for pre-flight checks. - Diagnosis engine timing: The
diagnosis-enginephase takes ~8 minutes due to 1032 row creates across 9 substeps. Do not interrupt mid-phase — it is resumable because of the SMART pattern, but re-running from scratch re-sends all 1032 rows (asunchanged). - ACME data:
data/ACME-ORG-DATA/contains 13 patient profiles, multilingual transcripts (de/en/fr), DICOM imaging samples, and full lab results for the ACME Hamburg demo clinic. This dataset is the primary sales demo asset. - Forbidden patterns (from
ddx-seeder-ts/CLAUDE.md): Direct Prisma/FHIR/MinIO calls are forbidden — all data flows through the REST API. Never usePOST /organizations— alwaysPOST /bootstrap/tenants.
Related Topics
- Prisma 7 Schemas — the seeder populates data that lands in all 5 Prisma schemas;
prisma-mainis the primary target - FHIR Partitions & Tenancy —
essentialsphase provisions FHIR partitions via the tenant bootstrap endpoint - Tenant Provisioner —
POST /bootstrap/tenantscalled byEssentialsSeedertriggers the full provisioning pipeline - User Management —
StaffSeedercreates practitioners + triggers FHIR Practitioner resource creation