Namespaces¶
A namespace is an independent mapping between original values and their pseudonyms. Each one fixes, at creation, how its pseudonyms are generated: the generation method, the pseudonym length, and an optional prefix and suffix. None of that can change once the namespace is created.
A namespace can also carry an optional regular expression every original value has to match before
a pseudonym is generated for it. Unlike the rest, the pattern can be edited afterwards, along with
the namespace's description - from the admin UI, or with NamespaceService.Update (see
Updating a namespace or project). A changed pattern only applies to original values
that are new to the namespace: a value that already has a pseudonym keeps resolving to it, and
creating it again returns the stored pseudonym, even if the value no longer matches.
Pseudonym generation methods¶
| Method | Pseudonyms |
|---|---|
PSEUDONYM_GENERATION_METHOD_SECURE_RANDOM_BASE64URL_ENCODED |
The default. The first 4 bytes of the SHA-256-hashed host name, the 4 least significant bytes of the millisecond tick count since system start, and pseudonymLength cryptographically secure random bytes, encoded as URL-safe Base64. |
PSEUDONYM_GENERATION_METHOD_FULL_RANDOM_HEX_ENCODED |
A cryptographically secure random string, hex-encoded. |
PSEUDONYM_GENERATION_METHOD_FULL_RANDOM_BASE62_ENCODED |
A cryptographically secure random string, Base62-encoded. |
PSEUDONYM_GENERATION_METHOD_FULL_RANDOM_BASE32_ENCODED |
A cryptographically secure random string, Base32-encoded with the RFC 4648 ยง6 alphabet. |
PSEUDONYM_GENERATION_METHOD_UUID4 |
A version 4 UUID (RFC 4122). |
PSEUDONYM_GENERATION_METHOD_UUID7 |
A version 7 UUID (RFC 9562). |
PSEUDONYM_GENERATION_METHOD_VOPRF |
Derived deterministically from the original value by a separate key-holding service that never sees it. See VOPRF pseudonyms. |
PSEUDONYM_GENERATION_METHOD_EXTERNAL |
Generates nothing: the pseudonyms are created by a third party and stored as given. See External pseudonyms. |
Every method except VOPRF and EXTERNAL is random: the same original value only maps to the same pseudonym
because vfps stores the pair. The full set of request fields is documented in
namespaces.proto.
External pseudonyms¶
A namespace created with PSEUDONYM_GENERATION_METHOD_EXTERNAL holds pseudonyms that were created
outside vfps, e.g. by a third party. pseudonymLength, pseudonymPrefix and pseudonymSuffix
don't apply: values are stored exactly as given. Import is the only API call that writes to such
a namespace, and it only works on one - Create and Append fail with FAILED_PRECONDITION, so a
pseudonym generated by vfps can never end up next to an external one. Resolve, Get, List and
the reverse lookup work as for any other namespace.
grpcurl \
-plaintext \
-d '{"namespace": "third-party", "originalValue": "to be stored", "pseudonymValue": "ext-1234"}' \
127.0.0.1:8081 \
vfps.api.v1.PseudonymService/Import
Over REST, this is POST /v1/namespaces/{namespace}/pseudonyms:import with the same body. It
requires the same write access as Create.
Nothing is ever overwritten. Importing a pair that is already stored succeeds and returns it, so a
retried call is safe. A pair that contradicts what is stored fails with ALREADY_EXISTS: the
original value already has a different pseudonym (unless the namespace allows multiple pseudonyms
per original value, in which case the new one is appended), or the pseudonym value already belongs
to a different original value. The error doesn't say which original value holds it. An original
value that doesn't match the namespace's validation regex fails with INVALID_ARGUMENT, and one
missing from a validating parent namespace with FAILED_PRECONDITION.
To load many pairs at once, use a CSV import job instead.
Multiple pseudonyms per original value¶
A namespace created with "allowsMultiplePseudonyms": true lets a single Create call store more
than one distinct pseudonym for the same original value, via the request's optional count field
(omitted or 1 preserves the default single-pseudonym behavior for every other namespace):
grpcurl \
-plaintext \
-d '{"name": "multi-psn-example", "pseudonymGenerationMethod": "PSEUDONYM_GENERATION_METHOD_FULL_RANDOM_HEX_ENCODED", "pseudonymLength": 32, "allowsMultiplePseudonyms": true}' \
127.0.0.1:8081 \
vfps.api.v1.NamespaceService/Create
grpcurl \
-plaintext \
-d '{"namespace": "multi-psn-example", "originalValue": "to be pseudonymized", "count": 3}' \
127.0.0.1:8081 \
vfps.api.v1.PseudonymService/Create
The response's pseudonyms array holds the full set (pseudonym is kept, populated with the
first one, for callers that only read a single value). The stored set for a given original value
only ever grows: calling Create again with a count at or below what's already stored returns
the existing set unchanged, while a larger count adds exactly the missing pseudonyms, up to a
maximum count of 10,000. CSV pseudonymization jobs and the FHIR $create-pseudonym operation
don't support requesting more than one pseudonym - both always operate on the first
(sequenceNumber: 0) pseudonym for a multi-psn namespace.
Appending a pseudonym¶
Create is idempotent: calling it again with the same count returns the same set. To store one
more pseudonym for an original value regardless of how many it already has, call Append
instead. Each successful call adds a newly generated pseudonym after the highest sequenceNumber
stored for the value, or stores the first one (sequenceNumber: 0) if none exists yet. The
response holds only the pseudonym that was just added:
grpcurl \
-plaintext \
-d '{"namespace": "multi-psn-example", "originalValue": "to be pseudonymized"}' \
127.0.0.1:8081 \
vfps.api.v1.PseudonymService/Append
Over REST, this is POST /v1/namespaces/{namespace}/pseudonyms:append. Append is not
idempotent by itself, so retrying a call that actually succeeded stores an extra pseudonym. To
retry safely, send a requestId (a UUID, generated once per append and reused only for its
retries): a call repeating the requestId of one that already succeeded returns that call's
pseudonym instead of storing another. See Retrying safely.
REQUEST_ID=$(uuidgen)
grpcurl \
-plaintext \
-d "{\"namespace\": \"multi-psn-example\", \"originalValue\": \"to be pseudonymized\", \"requestId\": \"$REQUEST_ID\"}" \
127.0.0.1:8081 \
vfps.api.v1.PseudonymService/Append
Append requires the same write access as Create, and it fails with FAILED_PRECONDITION on a
namespace that doesn't allow multiple pseudonyms. It is only available through the gRPC and REST
APIs.
Multi-level namespaces¶
Namespaces can form a hierarchy: a namespace created with a parentName is a child namespace,
whose original values are pseudonym values produced by its parent. This is how multiple levels of
pseudonymization are built - e.g. an MPI is pseudonymized in a root namespace, and that pseudonym
is then re-pseudonymized in a per-study child namespace, so a study never sees a value that
resolves directly to the MPI.
Each namespace keeps its own generation configuration; nothing is inherited from the parent.
Setting parentValidationMode to PARENT_VALIDATION_MODE_ENSURE_EXISTS additionally requires
every original value to already exist as a pseudonym in the parent namespace, rejecting anything
else with a FAILED_PRECONDITION error:
grpcurl \
-plaintext \
-d '{"name": "study-a", "pseudonymGenerationMethod": "PSEUDONYM_GENERATION_METHOD_FULL_RANDOM_HEX_ENCODED", "pseudonymLength": 32, "parentName": "test", "parentValidationMode": "PARENT_VALIDATION_MODE_ENSURE_EXISTS"}' \
127.0.0.1:8081 \
vfps.api.v1.NamespaceService/Create
Chaining a pseudonym into the child namespace is an ordinary Create call whose originalValue
is the parent's pseudonym value. A namespace's direct children can be listed with ListChildren
(GET /v1/namespaces/{name}/children), which is deliberately non-recursive - call it once per
level to walk a whole tree.
The parent link is set at creation and can't be changed afterwards, like every other namespace field, which also makes hierarchy cycles impossible. A namespace that still has children can't be deleted; delete the children first. Deleting a pseudonym level doesn't cascade to any other level.
Projects¶
A project is a named group of namespaces that belong together - e.g. the patient ID and the encounter ID namespaces of one study. It is how the admin UI shows, filters and lists them as a unit. A namespace can be part of any number of projects, or none, and can be added to or removed from a project at any time.
Projects are for organization only:
- They grant nothing. Access is still configured per namespace, so a project never widens or narrows who can read or write. Someone who can read only some of a project's namespaces sees just those, and doesn't see the project at all if they can read none of them (admins see every project, empty ones included).
- They are unrelated to the parent/child hierarchy, which says that one namespace's original values are another's pseudonyms. Namespaces in a project don't have to be related that way, and a project can mix namespaces from different hierarchies.
- Deleting a project doesn't touch its namespaces or their pseudonyms - they just stop being part of it. Deleting a namespace likewise removes it from its projects and leaves them in place.
Creating, editing and deleting projects and changing their namespaces requires admin access, like managing the namespaces themselves. Only a project's description can be edited; its name is its identity.
The ProjectService is available over gRPC and REST.
Creating projects at startup¶
Like namespaces, projects can be created at startup from the Init:v1:Projects configuration
section, listing the namespaces to put into each one. Projects are created after the namespaces, so
a project can name a namespace declared in the same configuration. An existing project isn't
overridden, but its listed namespaces are still added to it, and a namespace that doesn't exist is
logged and skipped.