Seeding and Demos
Seeding and Demos
Creating robust demo data is critical for E2E tests, documentation screenshots, and local development. This project follows specific best practices to ensure seeding is idempotent, deterministic, and maintainable.
0. The Seed Pipeline (SeedService.seed_all)
Every application startup runs a single ordered, idempotent seed pipeline against the dev/prod DB (backend/app/services/seed_service.py). The stage order is declared in _SEED_STAGE_NAMES so dependencies land first:
| # | Stage | Depends on |
|---|---|---|
| 1 | medications | — |
| 2 | clinical_event_types | — |
| 3 | allergies | — |
| 4 | body_parts (anatomy structures) | — |
| 5 | anatomy_figures | body_parts |
| 6 | concepts (taxonomy) | — |
| 7 | concept_edges (incl. concept→anatomy + anatomy→anatomy) | concepts + body_parts |
| 8 | default_catalog (units + biomarker definitions) | concepts (biomarker_class) |
| 9 | biomarker_panels (MEMBER_OF edges) | concepts + default_catalog |
Conventions:
- Envelope — every seed JSON is
{ "metadata": {...}, "items": [...] }. (The legacyanatomy_base.jsonwas split intoanatomy_structures.json+anatomy_relations.json; the latter is now deleted — anatomy hierarchy edges live inconcept_edges.jsonwithsrc_type=anatomy, dst_type=anatomy.biomarker_panels.jsonmigrated from a{panel: [slugs]}map to the standard items list.) - Stats contract — every
seed_*returns{added, updated, skipped, errors}.seed_default_catalogadditionally carries adetails: {units_added, units_updated, biomarkers_added, biomarkers_updated}sub-dict. - Idempotency + reconciliation — re-runs upsert by the natural key (slug /
(src, dst, relation)for edges) and reconcile existing rows to the JSON. Editing a concept'skindsarray and restarting will diff its kind tags into place; nothing is ever deleted by a seed. main.pycallsseed_all()in one place; the whole pipeline is wrapped in_abort_or_warn(fail-soft in dev, abort boot in prod).
To re-run a single stage against a running DB without restarting, see DEVELOPMENT.md §Seed System.
1. Avoid Raw SQL or .zip Imports for Core Seeds
While the system supports ZIP and JSON imports for end-users, relying on external .zip files for core developer seeding introduces complexity:
- Binary files are hard to review in Git.
- Schema changes break
.zippayloads silently until runtime. - Complex ID mapping is required.
Best Practice: Use the ImportService programmatically with an embedded FHIR Bundle in a Python script (like backend/scripts/seed_demo.py). This gives you:
- Complete control over deterministic dates.
- Type safety and stack traces if schema changes occur.
- Immediate feedback during development.
2. Deterministic Execution
Seeds must be idempotent. Running python3 scripts/seed_demo.py multiple times must not create duplicate tenants, users, or patients.
- Use
scalar_one_or_none()to check for existing records (e.g., matching a fixedslugoremail). - Check for existing data (e.g.,
Patient.mrnorObservation.subject) before bulk-inserting.
3. Date Freezing for Visual Regression
When generating data for screenshots (e.g., capture_ui.sh), dates must be relative to a "frozen" present.
In this project, the UI capture scripts (frontend/tests-e2e/ui-capture/capture.mjs) freeze the browser clock (e.g., to 2026-06-15).
Your seed script should generate FHIR resources with absolute dates relative to that same frozen point to ensure charts and relative times ("2 days ago") render identically across runs.
4. The Seed Data Structure
A robust clinical seed should use the internal ImportService.restore_fhir_bundle and include:
- Tenant & User: A dedicated demo tenant and at least one Admin user.
- Patients: At least one primary patient with a predictable MRN.
- Biomarkers:
Observationresources mapped to standard LOINC codes (the system will auto-resolve these). - Medications:
MedicationStatementresources. - Allergies:
AllergyIntoleranceresources. - Examinations: Sideloaded via
restore_sidecar("examinations.json", ...)to provide clinical context notes.
5. UI Patient Selection
By design, Admin users in Health Assistant have access to all patients in a tenant and do not have a default patient context automatically selected on login. If you see "No patient selected" in the Dashboard or AI Chat:
- During manual testing: Use the Patient Context Switcher (usually in the header or sidebar) to select your demo patient.
- During automated screenshots: The E2E script (
capture.mjs) automatically injects the primary patient into thepatient-storageZustand store viaaddInitScriptbefore rendering the page.
6. Anatomy Graph Catalog
The anatomy graph is a modular, directed-graph ontology of the human body. It replaces the old flat body_parts table with two interconnected models:
AnatomyStructure(nodes): body parts, organs, systems, tissues, cells, joints, etc.AnatomyRelation(edges): deleted — replaced byconcept_edgesrows (src_type='anatomy',dst_type='anatomy'). The relation types (PART_OF,BRANCH_OF,DRAINS_INTO,ARTICULATES_WITH,INNERVATED_BY,SUPPLIED_BY,CONTINUOUS_WITH) are nowConceptRelationTypevalues.
Each node carries a class_concept_slug referencing an anatomy_class concept (e.g. organ, system, region, organ-part, tissue, joint) and optional standard identifiers (standard_system + standard_code for SNOMED CT, LOINC, or CUSTOM coding). The legacy category enum (SYSTEM, REGION, ORGAN, …) was replaced by the unified taxonomy — the import service maps a class_concept_slug to a class_concept_id FK.
6.1 Automatic Seeding on Startup
The base anatomy catalog is seeded automatically on application startup by SeedService.seed_all() (stage body_parts). It reads one standard-envelope seed file for nodes:
backend/data/seeds/anatomy_structures.json (nodes — AnatomyStructure rows)Anatomy hierarchy edges are no longer a separate file — anatomy_relations.json is deleted; the edges now live in concept_edges.json (seeded at the concept_edges stage) with src_type=anatomy, dst_type=anatomy. The structures file contains 54 nodes (major body systems, organs, regions, and joints with SNOMED codes). The seeding is idempotent — existing nodes are updated by slug, new ones are inserted, and duplicate edges are skipped.
The startup log (emitted by seed_all) shows:
Seeding body_parts...
Seeded body_parts: {'added': 54, 'updated': 0, 'skipped': 0, 'errors': 0}6.2 Manual Re-seed
To re-seed the base anatomy catalog manually (e.g., after editing the split seed files), call the service method directly against a running backend:
cd backend
source venv/bin/activate
export PYTHONPATH=.:../
python -c "import asyncio; from app.core.database import AsyncSessionLocal; from app.services.seed_service import seed_service; asyncio.run(seed_service.seed_body_parts())"(The scripts/seed_anatomy.py CLI is for importing custom expansion packs — see §6.3 — not for re-seeding the base catalog.)
6.3 Importing Custom Expansion Packs
Custom anatomy packs (e.g., a detailed ophthalmology or neuroanatomy graph) can be imported from a local JSON file or a URL using the same script:
# From a local file
python scripts/seed_anatomy.py --file path/to/my-anatomy-pack.json
# From a URL
python scripts/seed_anatomy.py --url https://example.com/anatomy-pack.jsonIn Docker:
docker compose --env-file .env -f docker/docker-compose.standalone.yml exec backend python scripts/seed_anatomy.py --file /path/to/pack.jsonAlternatively, use the REST API endpoint POST /api/v1/anatomy/import (SYSTEM_ADMIN only) with the same JSON payload format.
{
"nodes": [
{
"slug": "left-ventricle",
"name": "Left Ventricle",
"class_concept_slug": "organ-part",
"standard_system": "snomed",
"standard_code": "87878005",
"description": "The lower left chamber of the heart",
"is_custom": false,
"display": {
"map": {
"markers": {
"man-front": { "nx": 0.55611, "ny": 0.35823, "nr": 0.06984 }
}
}
}
}
],
"edges": [
{
"source_slug": "left-ventricle",
"target_slug": "heart",
"relation_type": "PART_OF"
}
]
}Import rules:
- Nodes are upserted by
slug. If a node with the same slug exists, it is updated; otherwise, it is created. Records are never deleted. - Edges are deduplicated by the unique constraint
(source_id, target_id, relation_type). Duplicate edges are silently skipped. (The formerAnatomyRelationtable and this schema are replaced byconcept_edgesrows —src_type=anatomy, dst_type=anatomy.) - Edge
source_slugandtarget_slugmust resolve to existing nodes (either in the payload or already in the database). Unresolvable edges are skipped with a warning. class_concept_slugis optional and must resolve to an existinganatomy_classconcept (e.g.organ,system,region,organ-part,tissue,joint). This is the only accepted form — the legacy uppercasecategoryenum (SYSTEM,REGION,ORGAN, …) was removed (clean rebuild); seed files must carryclass_concept_slugdirectly.relation_typemust be one of:PART_OF,BRANCH_OF,DRAINS_INTO,ARTICULATES_WITH,INNERVATED_BY,SUPPLIED_BY,CONTINUOUS_WITH.standard_systemis optional and must be one of:loinc,snomed,custom.displayis optional JSONB metadata. It holds body-map rendering hints underdisplay.map.markers, keyed by figure slug (e.g.man-front,woman-back— slugs of rows in theanatomy_figurestable). Each marker uses normalized coordinates (nx,ny,nrin the 0–1 range) relative to that figure's own viewBox. Absolute pixel coordinates are not used — positions are figure-independent and resolved at render time. See §6.6.
6.4 AI-Driven Graph Expansion
The system includes an AI orchestrator for on-demand anatomy graph generation. System admins can ask the AI chatbot to generate a detailed anatomical sub-graph (e.g., "Generate the anatomy of the cardiovascular system in detail"). The AI uses the propose_anatomy_graph_generation tool, which creates a HITL (human-in-the-loop) review card. Upon user confirmation, the AI generates the graph nodes and edges, validates them, and imports them via the same JSON import pipeline.
The AI task type define_anatomy_graph can also be called directly via POST /api/v1/ai-assistance/assist with a user_input describing the target structure.
6.5 Adding New Seed Nodes
To extend the base anatomy catalog that ships with the application:
- Edit
backend/data/seeds/anatomy_structures.json(add nodes toitems) and/orbackend/data/seeds/concept_edges.json(add anatomy→anatomy edges toitems, withsrc_type=anatomy, dst_type=anatomy). - Ensure node
slugvalues are unique, and that edgesource_slug/target_slugreference existing slugs. - Restart the application (or run the manual re-seed command in §6.2). The upsert logic will add new nodes and edges without affecting existing data.
6.6 Body Diagram Atlas & Organ Markers
The Anatomy Explorer renders a 2D human body and overlays organ markers so users can locate structures visually. The atlas is fully DB-driven, image-based, and admin-manageable:
anatomy_figurestable — each row is one view of one figure (e.g.man-front,woman-back) backed by a WebP image stored on disk underUPLOAD_DIR/anatomy_figures/. No SVG markup or viewBox — the stored image IS the view. The four defaults ship as bundled WebP seeds (rasterized from the Wikimedia surface diagrams at 3×, CC BY-SA 3.0 — seeNOTICE) and are copied toUPLOAD_DIRat startup.- Per-figure markers — every structure may carry
display.map.markers[<figure-slug>] = { nx, ny, nr }, wherenx/ny/nrare fractions of the image's pixel dimensions (0–1). The marker overlay SVG uses the image'swidth × heightas its viewBox, so it aligns with the<img>by construction at any render size. - Frontend store —
useAnatomyAtlas(Zustand,frontend/src/components/anatomy/atlas.ts) fetches the figure list once and lazily fetches each image as an authenticated blob → object URL.
Atlas Manager (SYSTEM_ADMIN only) — /admin/anatomy-atlas. Create/edit/delete figures with an interactive image cropper: upload a source image, drag to select the view region (real pixel crop via <canvas> → WebP), preview live, and save — all without code changes. To add a new view (e.g. a left-side aspect): Add Figure → pick a figure group (man) and view (left) → upload the source image → crop → save. Markers can then be placed on the new figure via the Position Editor.
Position Editor (SYSTEM_ADMIN only) — the canonical way to place or fix a structure's marker. From the Anatomy Explorer, click Positions to open the drag-and-drop editor:
- Pick the figure from the picker (grouped by figure group → views).
- Search a structure in the sidebar and toggle its marker ON.
- Drag the dot onto the body — normalized coordinates update live and auto-save via
PATCH /api/v1/anatomy/{slug}(writingdisplay.map.markers[figure-slug]).
No pixel math or JSON editing is required.
7. Exporting an Instance Back to Seeds
Seed files are normally hand-edited JSON. But it's often easier to build the
canonical taxonomy / anatomy / biomarker catalog in a running instance — via
the Catalogs workspace, the Anatomy Atlas editor, the AI chatbot's graph-
generation tools, or the ontology-catalog import — and then snapshot that
instance's data back into the shipped seed format. SeedExportService
(backend/app/services/seed_export_service.py) is the strict inverse of
SeedService: it reads the DB and emits the slug-keyed {metadata, items}
envelope each seed_* stage consumes, covering all eight seed files —
concepts, concept edges, anatomy structures, the default catalog,
biomarker panels, clinical event types, medications, and allergies.
The output is deterministic (sorted by slug/name, fixed field order, only
meaningful fields, a regenerated metadata block), so re-exporting an
unchanged DB is a git no-op — the point is to review diffs, not blind-replace.
7.1 Identity model
Slug is the stable join key; name is mutable display. Cross-references
between seed items are by slug (parent_slug, src_slug/dst_slug,
panel_slug, class_concept_slug). Where an entity has a real external code,
coding_system + code are emitted alongside as identity evidence
(FHIR-aligned, useful for interop/dedup audits) but are not the join key —
see the design discussion in dev/plans/seed-export-service-2026-07-07.md.
7.2 Three delivery surfaces (same service underneath)
UI download (SYSTEM_ADMIN) — the Catalogs workspace toolbar
(/catalogs) has an Export seeds button that calls
GET /api/v1/admin/seeds/export.zip and saves a ZIP of the eight files. The
download is read-only — the server never writes its own data/seeds/.
CLI (curator on the same machine as the DB):
cd backend && source venv/bin/activate && export PYTHONPATH=.:../
python scripts/export_seeds.py --dry-run # preview counts, no writes
python scripts/export_seeds.py # global taxonomy -> data/seeds
python scripts/export_seeds.py --source TENANT_ID # a template tenant
python scripts/export_seeds.py --out /tmp/seeds # custom output dirThe write is safe: files stage to .export-staging/, existing files back up
to .backup-<timestamp>/, then atomic write.
Dev-side unpack helper — for the transfer workflow (instance on another machine): download the ZIP via the UI, copy it to your dev machine, then:
python scripts/unpack_seeds_zip.py path/to/health-assistant-seeds.zipBacks up existing data/seeds/*.json to .backup-<timestamp>/, then extracts.
Refuses archives containing unknown or path-traversal entries.
7.3 The transfer workflow (instance on another machine)
- On the running instance: Catalogs workspace → Export seeds →
health-assistant-seeds.zip. - Transfer the ZIP to your dev machine (download, scp, USB).
python scripts/unpack_seeds_zip.py health-assistant-seeds.zip(backup + extract).git diff data/seeds/→ review → commit. The next release ships the new seeds.
7.4 Source scope
--source global (the default) exports tenant_id IS NULL rows — the global
taxonomy that ships. Pass a tenant UUID (--source TENANT_ID or
SeedExportService(db, tenant_id=...)) to treat a dedicated "template tenant"
as the source: its rows are emitted with scope stripped. This is how you curate
the canonical set in a tenant you control via the UI, then promote it to global
seeds.
7.5 Binary anatomy assets note
Positional marker data lives in AnatomyStructure.display (JSONB) and is
carried by the anatomy_structures.json export. Binary anatomy_figures/
images are out of scope for the seed ZIP (managed via the Atlas Editor).
See backend/data/seeds/README.md for the per-file field-schema cheatsheet.