# Deterministic IDs (CDP-UUID) **Date:** 2026-03-27 **Status:** Current --- ## Table of Contents 1. [Overview](#1-overview) 2. [Algorithm](#2-algorithm) 3. [Cross-SDK Usage](#3-cross-sdk-usage) 4. [Cross-SDK Verification](#4-cross-sdk-verification) 5. [Record-Type URI Factories](#5-record-type-uri-factories) 6. [Design Notes](#6-design-notes) --- ## 1. Overview All Cascade Protocol records use **content-hashed deterministic URIs** — identifiers derived from the record's clinical content rather than assigned by a database or EHR system. The same immunization record imported from Epic and the same record imported from Cerner will produce the same URI, because the URI is computed from the clinical fields (vaccine code, patient, date) rather than the source system's internal ID. This property is the foundation of the multi-source deduplication workflow. The CLI's `--reconcile-existing` flag and the SDK reconciliation logic both rely on deterministic URIs to detect duplicates across imports. ### Why not UUIDs assigned at import time? Random UUIDs assigned on first write cannot detect that `epic-record-A` and `cerner-record-B` are the same clinical fact. Deterministic URIs solve this without requiring a central identity service or prior coordination between systems. ### Stability guarantee A record's URI is stable as long as its identity fields do not change. The identity fields for each record type are chosen to be stable clinical facts (date of vaccination, vaccine code, patient date-of-birth) rather than mutable administrative data. This means a Pod can be rebuilt from raw exports and produce the same URIs. --- ## 2. Algorithm All Cascade Protocol SDK implementations produce the same URI for a given set of inputs. The algorithm is: **Step 1 — Filter and normalize fields** Remove nil and empty-string values. Do not include fields that are mutable or source-specific (e.g., internal IDs, import timestamps, provenance metadata). **Step 2 — Build the identity string** Sort the remaining field keys alphabetically. Concatenate them in the format: ``` {ResourceType}::{key1=value1|key2=value2|...} ``` Example for an immunization: ``` ImmunizationRecord::cvx=140|date=2019-08-15|patientDob=1985-03-15 ``` **Step 3 — Compute SHA-1** Compute the SHA-1 hash of the identity string (UTF-8 encoded). SHA-1 is used for its output length properties; this is not a security-sensitive operation. **Step 4 — Format as UUID v5 layout** Take the first 16 bytes of the SHA-1 digest and format them as a UUID string with hyphens at the standard positions (8-4-4-4-12). Set the version nibble to `5` and the variant bits per RFC 4122. **Step 5 — Return as URN** Return the result as `urn:uuid:{uuid}`. ### Reference implementation ``` deterministicUuid("hello") === "aaf4c61d-dcc5-58a2-9abe-de0f3b482cd9" ``` All SDK implementations MUST produce this output for the input string `"hello"`. Use this as a conformance test when implementing or porting the algorithm. --- ## 3. Cross-SDK Usage ### TypeScript ```typescript import { contentHashedUri, patientUri, immunizationUri } from '@the-cascade-protocol/sdk'; // Generic content-hashed URI const uri = contentHashedUri('Patient', { dob: '1985-03-15', given: 'John', family: 'Smith', sex: 'male' }); // → urn:uuid:... // Convenience factories for common record types const patUri = patientUri({ dob: '1985-03-15', given: 'John', family: 'Smith', sex: 'male' }); const immUri = immunizationUri({ cvx: '140', date: '2019-08-15', patientDob: '1985-03-15' }); ``` ### Python ```python from cascade_protocol import content_hashed_uri, patient_uri, immunization_uri # Generic content-hashed URI uri = content_hashed_uri('Patient', { 'dob': '1985-03-15', 'given': 'John', 'family': 'Smith', 'sex': 'male' }) # → urn:uuid:... # Convenience factories pat_uri = patient_uri({'dob': '1985-03-15', 'given': 'John', 'family': 'Smith', 'sex': 'male'}) imm_uri = immunization_uri({'cvx': '140', 'date': '2019-08-15', 'patient_dob': '1985-03-15'}) ``` ### Swift ```swift import CascadeSDK // Generic content-hashed URI let uri = DeterministicURI.contentHashedURI( resourceType: "Patient", fields: ["dob": "1985-03-15", "given": "John", "family": "Smith", "sex": "male"] ) // → urn:uuid:... // Convenience factory via makeDeterministic() on record types let immunization = ImmunizationRecord(cvx: "140", date: date, patientDob: patientDob) let deterministicImmunization = immunization.makeDeterministic() // deterministicImmunization.uri is the content-hashed URN ``` --- ## 4. Cross-SDK Verification All SDK implementations must produce identical output for the same inputs. The canonical test vector is: | Input | Expected output | |-------|----------------| | `"hello"` | `aaf4c61d-dcc5-58a2-9abe-de0f3b482cd9` | Verification examples: ```typescript // TypeScript import { deterministicUuid } from '@the-cascade-protocol/sdk'; assert(deterministicUuid('hello') === 'aaf4c61d-dcc5-58a2-9abe-de0f3b482cd9'); ``` ```python # Python from cascade_protocol import deterministic_uuid assert deterministic_uuid('hello') == 'aaf4c61d-dcc5-58a2-9abe-de0f3b482cd9' ``` ```swift // Swift let result = DeterministicURI.deterministicUuid("hello") assert(result == "aaf4c61d-dcc5-58a2-9abe-de0f3b482cd9") ``` If an SDK implementation produces a different result for `"hello"`, the algorithm is not conformant and cross-SDK deduplication will not work correctly. --- ## 5. Record-Type URI Factories Each SDK provides convenience factories for common record types. These wrap `contentHashedUri` with the correct field selection for each type. | Record type | Identity fields used | |-------------|---------------------| | `Patient` | `dob`, `given`, `family`, `sex` | | `ImmunizationRecord` | `cvx`, `date`, `patientDob` | | `Medication` | `rxNorm`, `startDate`, `patientDob` | | `ConditionRecord` | `snomedCode`, `onsetDate`, `patientDob` | | `LabResultRecord` | `loincCode`, `date`, `patientDob` | | `AllergyRecord` | `allergen`, `onsetDate`, `patientDob` | | `VitalSignRecord` | `loincCode`, `date`, `value`, `patientDob` | | `ProcedureRecord` | `snomedCode`, `date`, `patientDob` | The field selection for each type is designed to be stable across EHR systems and robust to minor data variations. If a record is missing one or more identity fields, the factory falls back to a random UUID and logs a warning — a deterministic URI cannot be computed without the required fields. --- ## 6. Design Notes ### SHA-1 vs UUID v5 namespace Standard UUID v5 uses a namespace UUID as the first input to the hash function (RFC 4122, Section 4.3). CDP-UUID omits the namespace step and applies SHA-1 directly to the identity string. This means CDP-UUIDs are not RFC 4122 UUID v5 values, even though they use the UUID v5 byte layout. The version nibble is set to `5` for format compatibility with UUID validators. ### Why SHA-1? SHA-1 produces 160 bits; truncated to 128 bits for the UUID, collision probability is negligible for the expected cardinality of a personal health record pod (tens of thousands of records at most). SHA-1 is used here purely for its output size, not for cryptographic security. ### Determinism across languages String encoding must be UTF-8 in all implementations. Key sorting must be byte-order ascending (the default for most language sort functions on ASCII keys). Any deviation in encoding or sort order will produce different URIs for the same logical record. --- *See also:* - [Multi-Source EHR Import](./multi-source-import.md) - [CLI Reference](./cli-reference.md) - [Serialization Specification](../spec/serialization/index.md)