Skip to content

PII Redaction

When designing zero-knowledge audit trails, you need to decide how to handle Personally Identifiable Information (PII) and Protected Health Information (PHI). Volidator provides two distinct strategies:

Requirement Strategy Configuration How it works
PII must never leave your servers, and showing placeholder labels in the dashboard is acceptable. Static Redaction redactKeys The SDK permanently strips fields, replacing them with a static redaction string before encrypting.
PII must never leave your servers, but the dashboard must display real names to authorized viewers. JIT Hydration referenceKeys The SDK replaces values with non-sensitive reference IDs, which the host app resolves on the fly inside the browser.
Store PII/PHI in Volidator, but guarantee Volidator cannot read it. End-to-End Encryption (E2EE) None (Default) Fields are encrypted with AES-GCM client-side. Volidator stores the ciphertext. Even if Volidator is compromised, the data is mathematically unreadable without your local keys.

[!NOTE]
Zero-Knowledge Guarantee: In all three scenarios, Volidator cannot read your data. There is zero chance of a data breach exposing your plaintext payloads as long as your Data Encryption Key (DEK) is not compromised.


redactKeys lets you replace sensitive field values with a [REDACTED:fieldName] placeholder before encryption. The blind index for that field is still computed from the original value, so queries still work. The stored ciphertext never contains the plaintext.

const volidator = new VolidatorClient({
apiKey: process.env.VOLIDATOR_API_KEY!,
encryptionKey: process.env.VOLIDATOR_ENCRYPTION_KEY!,
redactKeys: ['actor', 'metadata.email', 'metadata.ssn'],
});

Supported top-level fields: actor, target.

// Input
await volidator.log({
actor: 'user@example.com',
action: 'account.login',
target: 'account_456',
});
// Encrypted payload contains:
// { actor: "[REDACTED:actor]", action: "account.login", target: "account_456" }
// actorBlindIndex is still computed from "user@example.com"

Use dot notation to target fields inside metadata.

const volidator = new VolidatorClient({
apiKey: process.env.VOLIDATOR_API_KEY!,
encryptionKey: process.env.VOLIDATOR_ENCRYPTION_KEY!,
redactKeys: ['metadata.email', 'metadata.phone', 'metadata.dob'],
});
await volidator.log({
actor: session.userId,
action: 'patient.view',
metadata: {
email: 'patient@example.com', // stored as [REDACTED:email]
phone: '+1-555-0100', // stored as [REDACTED:phone]
dob: '1985-03-12', // stored as [REDACTED:dob]
appointmentId: 'appt_789', // not redacted — no rule for this key
},
});

Only string metadata values are redacted. Numbers, booleans, and objects are passed through unchanged. Apply your own stringification if needed before logging.

Redaction happens after blind index computation. The SDK processes in this order:

  1. Compute actorBlindIndex from the original actor value.
  2. Compute actionBlindIndex from the original action value.
  3. Compute targetBlindIndex (if target is provided) from the original target value.
  4. Compute tenantBlindIndex (if tenant is provided) from the original tenant value.
  5. Apply redactKeys and referenceKeys substitutions to the payload.
  6. Encrypt the redacted/referenced payload.
  7. Send { actorBlindIndex, actionBlindIndex, targetBlindIndex, tenantBlindIndex, encryptedPayload }.

This means filters still work even when fields are redacted.

Redaction reduces stored PII but does not eliminate it entirely:

  • Action names (not currently redactable) may contain identifiers if you name actions like user:email@example.com.login.
  • Blind indexes are deterministic. If an attacker has the key, they can confirm whether a specific value is in the index.

For HIPAA and GDPR compliance, use redactKeys for all PHI/PII fields and keep your encryption key in a secrets manager with rotation policies.

For audit dashboards where compliance requires that PII/PHI never leave your boundary, but human auditability requires displaying actual names (e.g., “John Doe exported logs” instead of [REDACTED:actor]), use JIT Hydration.

With JIT Hydration:

  • The SDK receives a structured { id, pii } payload.
  • The search index is built using pii, but only [REF:id] is stored.
  • The host page hydrates the display name on the fly via a React hook.

For a complete walkthrough, see the JIT Hydration Guide.