Export & Import (Backup)
Export & Import (Backup) System
Health Assistant can export and re-import data as backup files, at three scopes and in three formats. Exports are FHIR R4B-conformant and integrity-checked; imports are validated with fhir.resources and upserted by natural key with automatic id remapping across tenants.
Files:
backend/app/services/{export_service,import_service,fhir_converter}.py,backend/app/api/v1/endpoints/{export,import_data}.py,backend/app/workers/tasks.py(export_backup,import_backup),backend/app/models/export_import_job.py,backend/app/schemas/backup.py.
1. Scopes
Scopes mirror the FHIR Bulk Data Access levels. Each scope is stored on the export job as a SMART-compatible scope string (smart_scope) — these are vocabulary-only claims enforced by the platform's existing RBAC + tenant isolation; no separate OAuth2 authorization server is required.
| Scope | SMART claim | Who can request | Coverage |
|---|---|---|---|
patient | patient/*.rs | USER (single own patient), MANAGER, ADMIN, SYSTEM_ADMIN | One patient (patient_ids required, single id for USER) |
group | system/*.rs | MANAGER, ADMIN, SYSTEM_ADMIN | Multiple patients (patient_ids required) |
system | system/*.cruds | ADMIN, SYSTEM_ADMIN | Whole tenant (patient_ids ignored) |
SYSTEM_ADMIN bypasses all role checks (existing behaviour).
2. Export types
| Type | File | Contents |
|---|---|---|
fhir_only | <job_id>.fhir.json | A single FHIR R4B Bundle (type: transaction). Portable to any FHIR server. Resources: Patient, Observation, MedicationStatement, AllergyIntolerance, DiagnosticReport, Organization, Practitioner, DocumentReference (metadata only — no inline binaries). No telemetry, no integrations, no AI config. |
full_backup | <job_id>.zip | A BagIt-style ZIP containing the FHIR Bundle plus non-FHIR sidecars, raw document files, and a SHA256 manifest. Re-importing this ZIP restores the tenant/patient. |
catalog_only | <job_id>.catalog.json | Biomarker/unit definitions, medication/allergy catalogs + clinical-event-type metadata_schema definitions. Cross-tenant portable (use to seed a new deployment). |
ZIP layout (full_backup)
<job_id>.zip
├── manifest.json # BackupManifest: schema_version, exported_at, tenant_id, fhir_version, scope, export_type, smart_scope, counts{}, files[]{path,sha256,size}, options, notes
├── manifest-sha256.txt # <sha256> <path> (BagIt-style, one line per file)
├── bag-info.txt # tenant id, job id, export timestamp, schema/FHIR version, smart scope
├── fhir/
│ └── bundle.json # the FHIR R4B transaction Bundle (clinical core)
├── nonfhir/
│ ├── examinations.json
│ ├── clinical_events.json
│ ├── clinical_event_types.json
│ ├── biomarker_definitions.json
│ ├── medication_catalog.json
│ ├── allergy_catalog.json
│ ├── documents.json # document metadata + _archive_path mapping
│ ├── telemetry.json # group/system scope only (see §5)
│ ├── integrations.json # user_integrations incl. encrypted user_config + OAuth tokens
│ ├── notification_triggers.json
│ └── ai_config.json # system scope + include_ai_config only (restore not supported in v1)
└── documents/ # raw uploaded files (not base64), referenced by documents.json
└── <doc_id>.<ext>3. Endpoints
All under /api/v1, JWT-auth required.
Export
| Method | Path | Purpose |
|---|---|---|
POST | /export | Create an export job (body: BackupRequest). Enqueues export_backup Celery task. Returns ExportJobResponse. |
GET | /export/jobs | List export jobs for the current tenant. |
GET | /export/jobs/{job_id} | Get job status (PENDING → PROCESSING → COMPLETED/FAILED/PARTIAL). |
GET | /export/jobs/{job_id}/download | Download the generated file (JWT-gated, tenant-scoped). |
POST /export body:
{
"scope": "patient",
"export_type": "full_backup",
"patient_ids": ["<uuid>"],
"include_documents": true,
"include_telemetry": true,
"include_integrations": true,
"include_ai_config": false
}Import
| Method | Path | Purpose |
|---|---|---|
POST | /import/backup | Upload a ZIP or a bare bundle.json/catalog.json. Enqueues import_backup Celery task. Returns ImportJobResponse. |
GET | /import/jobs/{job_id} | Get backup-import job status + restore_result (created/updated counts, manifest verification). |
POST | /import/fhir | Synchronous FHIR Bundle import (smaller bundles, no job tracking). |
POST | /import/csv | CSV import (legacy). |
POST | /import/ocr | OCR extraction (legacy). |
POST /import/backup — multipart file field. The server detects the format:
- ZIP → verifies manifest SHA256, restores FHIR bundle + sidecars + documents.
- Bare JSON with
resourceType: "Bundle"→ restores as a FHIR transaction. - Bare JSON with
units/biomarkerskeys → upserts the ontology catalog.
4. FHIR conversion & validation
- Library:
fhir.resources==8.2.0(R4B sub-package, Pydantic-v2-native). Provides structural validation (types, cardinality, JSON shape) without a terminology server. services/fhir_converter.pyconverts ORMto_dict()output (snake_case + app extensions) to canonical FHIR R4B JSON and back:- App-only fields (
tenant_id,user_id,biomarker_id,normalized_value,dashboard_layout,org_type, …) are stripped from the FHIR output. Patient.mrn→identifier[{system: "urn:healthassistant:mrn"}].- Enum casing normalized (
Gender.FEMALE→"female";MedicationStatus.ACTIVE→"active"). MedicationORM → FHIRMedicationStatement;DoctorModel→ FHIRPractitioner.- Cardinality fixes for R4B:
Organization.type,DiagnosticReport.conclusionCode/presentedForm/categorywrapped as lists;Observation.comment→note;MedicationStatement.timing.repeat.timeOfDaypadded toHH:MM:SS. (Observation.interpretationis now stored natively as the canonical FHIR0..*CodeableConcept list — JSONB column — so export is a pass-through;Observation.componentis also stored + exported natively.)
- App-only fields (
- Every exported resource carries
meta.versionId,meta.lastUpdated,meta.source, and a provenancemeta.tag({system: "https://healthassistant.local/fhir/export", code: "ha-export"}). - On import, each entry is validated with
fhir.resources.R4B.get_fhir_model_class(rt).model_validate(...); invalid entries are recorded as errors and skipped. All 15 registered FHIR resource types are importable (Patient,Observation,MedicationStatement,MedicationRequest,AllergyIntolerance,DiagnosticReport,Organization,Practitioner,Condition,Encounter,Device,Communication,Provenance;DocumentReferenceis intentionally skipped on import with a warning since binary content lives in thedocuments/sidecar).
5. Data integrity
- SHA256 manifest:
manifest.jsonlists every file with itssha256+size;manifest-sha256.txtis the BagIt-style"<sha256> <path>"duplicate. On import, each file's hash is recomputed and compared →manifest_verifiedflag on the result. - Provenance tag: every FHIR resource carries the
ha-exportmeta.tag; importers can check it to confirm origin. - Round-trip: export serializes via
fhir.resources; import validates with the same R4B classes, so the schema is shared. Pydantic field equality + hash match is the integrity guarantee.
6. Restore behaviour & caveats
- Transaction verb routing: the import path honors each entry's
request.method(FHIR transaction Bundles).PUT Type/<id>updates an existing same-tenant row or creates one with the supplied id (not a random UUID);POST+ifNoneExistperforms a conditional create (skipped when a match exists — Patientidentifier→mrnlookup today; unsupported forms log a warning and create rather than silently treating "couldn't match" as "no match");DELETEsoft-deletes (idempotent on missing id). Entries with norequestblock default toPOST(create-new) — the historical behaviour. This makes round-tripping third-party EHR / SMART transaction Bundles preserve referential integrity. - Upsert by id with cross-tenant remapping: for each FHIR resource, if the imported
idalready exists in the same tenant → update in place; if it exists in a different tenant → generate a new UUID, record the remap, and surface a warning in the ImportJob result (e.g."Patient/<id> already exists in tenant <other>; created new with id <new>") so collisions are visible. All reference-bearing fields (subject/patient/performer/partOf/context/encounter/author/device/specimen/sender/recipient) are rewritten through the remap dict; bareurn:uuid:references are routed to the correct resource type via aFIELD_HINT_TO_TYPEmap + bundle look-ahead (ambiguous hints likesender/recipientare resolved by finding the referenced resource's type in the bundle). - Provenance per entry: every created/updated/deleted entry records a
Provenanceresource (best-effort — never aborts the import) with theCREATE/UPDATE/DELETEactivity code, the importing user as the agent, and anImportJob/<id>reference inentity_inputsso bulk-import Provenance is distinguishable from facade-write Provenance. BundleRestoreResult:ImportService.restore_fhir_bundlereturns a dataclass withcreated/updated/deleted/skipped/errors/warnings/id_remapfields (attribute access). Breaking change (was a 5-tuple prior to v0.3.0).- Telemetry: the
telemetry_datahypertable has nopatient_idcolumn, so for patient scope telemetry is excluded (a note is written to the manifest). Forgroup/systemscope, all tenant telemetry is included. On restore, telemetry rows are re-inserted with the destination tenant id. - Documents: raw files are archived under
documents/<doc_id>.<ext>; on restore they're written back toUPLOAD_DIR/<tenant_id>/<new-uuid>.<ext>andDocumentModel.file_pathis rebased. OCRextracted_text/entitiesare preserved (no re-OCR needed). - Integrations:
user_integrations.user_configis exported with Fernet-encrypted secrets intact (the{"_encrypted": ...}tokens) plus plaintext OAuthaccess_token/refresh_token. Restoring on a deployment with a differentINTEGRATION_SECRET_KEYwill leave secrets undecryptable — the rows import, but the integration will fail to authenticate. A warning is added ifINTEGRATION_SECRET_KEYis unset on the target. Never use themask_fieldshelper in the export path. - AI config (
ai_config.json): exported only whenscope=systemandinclude_ai_config=true. Restore is not supported in v1 (export-only) — a warning is recorded.api_keyvalues are exported as-is (they're already plaintext in the DB). - Clinical event types / categories: upserted by
slug(only inserted if missing; existing definitions are not overwritten). - Biomarker catalog: delegated to
CatalogImportService.import_catalog(upsert byslug/symbol).
7. Job lifecycle (Celery)
Export/import are async Celery tasks (long jobs don't block the request):
POST /export(or/import/backup) creates anExportJobModel/ImportJobModelrow (PENDING) and callsexport_backup.delay(job_id)/import_backup.delay(job_id, archive_path, user_id).- The task binds a fresh session to the worker-scoped shared engine via
get_async_session()(audit A7 — engine is a singleton withNullPool, never disposed per task), runsExportService.run_export/ImportService.run_import, and updates the job row (PROCESSING→COMPLETED/PARTIAL/FAILED). - The import task deletes the temp upload file in
finally. - Poll
GET /export/jobs/{id}(or/import/jobs/{id}) for progress; download viaGET /export/jobs/{id}/download.
Generated files are written to UPLOAD_DIR/exports/<tenant_id>/. There is no automatic retention/cleanup yet (admin should prune old exports).
8. Adding it to a new deployment
fhir.resources is in backend/requirements.txt. The migration 2f60048dd5ec_add_export_import_jobs_tables creates the export_jobs and import_jobs tables. Run alembic upgrade head after pulling.
9. Frontend UI
A production-ready page is available at /settings/export-import (admin-only — ADMIN and SYSTEM_ADMIN roles). It's wired into the Sidebar under Settings → Export & Import.
Files: frontend/src/pages/Settings/ExportImport.tsx, frontend/src/services/backupService.ts (named async fns wrapping the REST endpoints), frontend/src/services/backupService.test.ts (vitest), frontend/src/types/backup.ts (TS mirrors of the backend Pydantic schemas). Route registered in frontend/src/App.tsx (wrapped in the (user?.role === 'ADMIN' || user?.role === 'SYSTEM_ADMIN') block). Sidebar entry in frontend/src/components/layout/Sidebar.tsx with roles: ['ADMIN', 'SYSTEM_ADMIN']. i18n keys under the backup namespace in frontend/src/locales/{en,el}/common.json.
Page layout
- Tabbed interface (inline-tab pattern from
AIConfig): Export and Import. - Export tab: scope
<select>(patient/group/system— options disabled if the user lacks the role), format<select>(fhir_only/full_backup/catalog_onlywith inline descriptions), patient multi-select (radio forpatientscope, checkboxes forgroup;USERrole sees their linked patient as a read-only note), option toggles forinclude_documents/include_telemetry/include_integrations/include_ai_config(shown only forfull_backup, AI config only forSYSTEM_ADMIN+systemscope), and a Start export button that enqueues the Celery task. - Import tab: a dashed-border dropzone accepting
.zip/.json, an info panel explaining the format/tenant/secret-key caveats, and a confirmation modal (useUIStore.showConfirmationwithconfirmVariant: 'danger') before the restore is actually fired — protecting against accidental overwrites. - Job tables: both tabs show a job history table (created, scope/type, status badge with inline progress bar, size, resource counts, download button). The import table additionally shows
manifest_verified/fhir_validatedticks and error/warning counts. - Polling: when a job is created, the page starts a 3s
setIntervalpoller (the existing project pattern fromExaminationDetail.tsx) keyed on the job id, updates the row in place, stops when status reachesCOMPLETED/FAILED/PARTIAL, fires areact-toastifytoast, and self-clears after a 5-minute stall timeout. Intervals are tracked in auseRefmap and cleaned up on unmount. - Toasts:
toast.success/toast.error(err.response?.data?.detail || ...)per the project convention. - Download: uses the
triggerDocumentDownloadblob pattern fromdocumentService.ts—api.get(..., { responseType: 'blob' })→window.URL.createObjectURL→ synthetic<a>click.
Verification
npx tsc --noEmit— passes.npm run lint— 0 errors / 0 warnings in the new files.npm run build— passes (tsc + vite build + SW).npx vitest run— 22 tests pass (6 new inbackupService.test.ts+ 16 existing).
No frontend store was added — the page is stateless and tracks everything in local useState, matching the admin-page convention (CatalogManagement, TenantManagement).