FHIR R4 Database
The fhir_r4_db library provides a local database solution for storing and managing FHIR resources in Dart and Flutter applications. Built on Drift (SQLite), it offers type-safe CRUD operations, FHIR search parameter indexing, resource versioning, sync tracking, and optional encryption via SQLCipher.
Installation
dependencies:
fhir_r4_db: ^1.0.0
fhir_r4: ^0.4.4
drift: ^2.29.0
sqlite3: ^2.4.6
For Flutter apps you will also need a platform-appropriate SQLite opener. See the Drift documentation for platform-specific setup.
Key Features
- SQLite-backed: Persistent, file-based storage with excellent performance
- Search parameter indexing: Automatic indexing of FHIR search parameters (string, token, reference, date, number, quantity, URI, composite, special)
- Version tracking: Automatic versioning of resources with full history
- Sync support: Track resources that need syncing to a remote server
- Canonical resource cache: Store and retrieve canonical resources (ValueSet, StructureDefinition, etc.) by URL
- Optional encryption: SQLCipher support for encrypted databases
- Cross-platform: Works on all platforms supported by Drift (Android, iOS, macOS, Windows, Linux, Web)
Architecture
The database uses Drift's DAO (Data Access Object) pattern:
FhirDb— The Drift database class. You create it by passing aQueryExecutor(e.g.,NativeDatabase).FhirDao— The DAO that provides all FHIR operations (CRUD, search, history, sync, canonical resources, general storage).
Initialization
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:fhir_r4_db/fhir_r4_db.dart';
import 'dart:io';
// Create a database with a file-based SQLite backend
final db = FhirDb(NativeDatabase(File('fhir.db')));
// Access the DAO for all operations
final dao = db.fhirDao;
// For tests, use an in-memory database
final testDb = FhirDb(NativeDatabase.memory());
final testDao = testDb.fhirDao;
Encrypted Database
For encryption, use SQLCipher via the sqlite3 package:
import 'package:fhir_r4_db/fhir_r4_db.dart';
// Use cipherFromKey to create an encrypted database setup callback
final setup = cipherFromKey('your-encryption-key');
final db = FhirDb(NativeDatabase(File('fhir.db'), setup: setup));
Basic Operations
Saving Resources
import 'package:fhir_r4/fhir_r4.dart';
// Save a single resource
final patient = Patient(
name: [
HumanName(
family: 'Doe'.toFhirString,
given: ['John'.toFhirString],
),
],
);
// save() assigns an ID if none exists and updates meta (versionId, lastUpdated)
final savedPatient = await dao.saveResource(patient);
// Save multiple resources in a batch (more efficient)
final resources = [patient1, observation1, condition1];
final success = await dao.saveResources(resources);
Reading Resources
// Retrieve a resource by type and ID
final patient = await dao.getResource(
R4ResourceType.Patient,
'12345',
);
// Check if a resource exists
final exists = await dao.exists(
R4ResourceType.Patient,
'12345',
);
// Get all resources of a type
final allPatients = await dao.getResourcesByType(
R4ResourceType.Patient,
);
// Paginated retrieval
final page = await dao.getResourcesWithPagination(
resourceType: R4ResourceType.Patient,
limit: 20,
offset: 0,
);
// Count resources of a type
final count = await dao.getResourceCount(R4ResourceType.Patient);
// List all resource types in the database
final types = await dao.getResourceTypes();
Searching Resources
The database supports FHIR search parameters with automatic indexing:
// Search patients by name
final patients = await dao.search(
resourceType: R4ResourceType.Patient,
parameters: {'name': 'Doe'},
);
// Search with multiple parameters
final observations = await dao.search(
resourceType: R4ResourceType.Observation,
parameters: {
'patient': 'Patient/12345',
'code': 'http://loinc.org|85354-9',
'date': 'ge2024-01-01',
},
);
// Count search results
final resultCount = await dao.searchCount(
resourceType: R4ResourceType.Observation,
parameters: {'patient': 'Patient/12345'},
);
Supported search parameter types:
- String (
name,address, etc.) — case-insensitive prefix matching - Token (
code,identifier, etc.) — system|code matching - Reference (
patient,subject, etc.) — reference resolution - Date (
date,birthdate, etc.) — date range comparisons (eq, ne, gt, lt, ge, le, sa, eb, ap) - Number (
length, etc.) — numeric comparisons - Quantity (
value-quantity, etc.) — quantity with unit matching - URI (
url, etc.) — URI matching - Composite — combined parameter searches
- Special (
_has,_tag,_profile,_security,_source,_lastUpdated) — special FHIR parameters
Deleting Resources
// Delete a resource by type and ID
final deleted = await dao.deleteResource(
R4ResourceType.Patient,
'12345',
);
Advanced Features
Resource Versioning and History
The database automatically manages resource versions on each save:
// Save creates version 1
final v1 = await dao.saveResource(patient);
print(v1.meta?.versionId); // "1"
// Saving again increments the version
final updated = v1.copyWith(active: FhirBoolean(true));
final v2 = await dao.saveResource(updated);
print(v2.meta?.versionId); // "2"
// Retrieve version history
final history = await dao.getResourceHistory(
R4ResourceType.Patient,
'12345',
);
// Returns all versions, most recent first
Use timestamp-based versioning instead of incrementing integers:
dao.versionIdAsTime = true;
Sync Support
Track resources that need syncing to a remote server:
// Enable sync tracking
dao.storeForSync = true;
// Save resources — they'll be queued for sync automatically
await dao.saveResource(patient);
// Retrieve resources pending sync
final pendingSync = await dao.getSync();
// Clear the sync queue after successful upload
await dao.clearSync();
Canonical Resource Cache
Store canonical resources (ValueSet, StructureDefinition, etc.) indexed by URL:
// Save a canonical resource
await dao.saveCanonicalResource(valueSet);
// Retrieve by URL
final vs = await dao.getCanonicalResource(
'http://example.org/fhir/ValueSet/my-codes',
);
// Check if a canonical key exists
final hasIt = await dao.containsCanonicalKey(
'http://example.org/fhir/ValueSet/my-codes',
);
// List all canonical keys
final keys = await dao.listCanonicalKeys();
General Storage
Store arbitrary data (non-FHIR) in the database:
// Save arbitrary data (returns an integer key)
final key = await dao.saveGeneral(value: '{"name": "Test"}');
// Read by key
final data = await dao.readGeneral(key);
// Get all general storage entries
final allEntries = await dao.getAllGeneral();
// Delete by key
await dao.deleteFromGeneral(key);
// Clear all general storage
await dao.clearGeneral();
Example
A complete example using the database in a Dart application:
import 'dart:io';
import 'package:drift/native.dart';
import 'package:fhir_r4/fhir_r4.dart';
import 'package:fhir_r4_db/fhir_r4_db.dart';
Future<void> main() async {
// Create the database
final db = FhirDb(NativeDatabase(File('example.db')));
final dao = db.fhirDao;
// Save a patient
final patient = Patient(
name: [
HumanName(
family: 'Doe'.toFhirString,
given: ['John'.toFhirString],
),
],
birthDate: FhirDate('1970-01-01'),
);
final saved = await dao.saveResource(patient);
print('Saved patient: ${saved.id}');
// Search for patients
final results = await dao.search(
resourceType: R4ResourceType.Patient,
parameters: {'name': 'Doe'},
);
print('Found ${results.length} patients');
// Get history
final history = await dao.getResourceHistory(
R4ResourceType.Patient,
saved.id!.valueString!,
);
print('${history.length} versions');
// Clean up
await db.close();
}
Performance
The SQLite backend provides excellent performance. On a test machine (AMD Ryzen 7 PRO, 64 GB RAM, Linux), loading the MIMIC-IV Clinical Database Demo on FHIR (876 MB, ~899,000 resources) takes about 4 minutes, and searching for individual resources completes in under 10ms.