Skip to main content

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 a QueryExecutor (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.