Skip to content

Just-in-Time (JIT) Hydration

By default, Volidator operates as a zero-knowledge system: sensitive fields like actor or target are encrypted client-side or statically redacted (e.g., replaced with [REDACTED:actor]) before they are stored on Volidator servers.

However, showing [REDACTED:actor] to compliance officers or admins makes audit trails difficult to read.

Just-in-Time (JIT) Hydration (also known as Reference-Based Redaction) solves this by replacing sensitive values with non-sensitive reference IDs (e.g., database primary keys like usr_890). When the dashboard is embedded in your application, it asks the host application to resolve these IDs on the fly in the browser via a secure postMessage handshake.

┌─────────────────┐ ┌───────────────────┐ ┌────────────────┐
│ Host Portal │ │ Volidator Iframe │ │ Volidator API │
└────────┬────────┘ └─────────┬─────────┘ └───────┬────────┘
│ │ │
│ │ Get Encrypted Logs │
│ ├──────────────────────────────>│
│ │ │
│ │ Decrypted: "[REF:usr_890]" │
│ |<──────────────────────────────┤
│ │ │
│ VOLIDATOR_RESOLVE_ACTORS │ │
│ ["usr_890"] │ │
|<───────────────────────────────┤ │
│ │ │
│ Resolve "usr_890" -> "John" │ │
├──────────────────────────────┐ │ │
│ │ │ │
│<─────────────────────────────┘ │ │
│ │ │
│ VOLIDATOR_RESOLVE_RESPONSE │ │
│ { usr_890: { name: "John" } }│ │
├───────────────────────────────>│ │
│ │ │
│ │ Renders "John" │
│ ├───────────────┐ │
│ │ │ │
│ │<──────────────┘ │

Specify which fields should use reference-based redaction using the referenceKeys option in your VolidatorClient constructor.

import { VolidatorClient } from '@volidator/node';
const volidator = new VolidatorClient({
apiKey: process.env.VOLIDATOR_API_KEY!,
encryptionKey: process.env.VOLIDATOR_ENCRYPTION_KEY!,
// Fields that will be stored as references
referenceKeys: ['actor', 'metadata.ownerId'],
});

When logging, pass a ReferencePayload object { id, pii } instead of a plain string for any field listed in referenceKeys:

await volidator.log({
// 'actor' is a referenceKey
actor: {
id: 'usr_890', // Stored as "[REF:usr_890]"
pii: '[email protected]' // Only used to compute the blind search index
},
action: 'invoice.download',
metadata: {
// 'metadata.ownerId' is a referenceKey
ownerId: {
id: 'usr_456',
}
}
});

[!NOTE] The blind index is computed using the raw pii value ([email protected]). This ensures that searching by the actor’s real email in the dashboard still returns the correct logs, even though the plaintext email never leaves your system.


When generating an embed token, you must pass the exact hostOrigin of the parent application embedding the iframe. This enables strict origin validation for the postMessage handshake, preventing unauthorized pages from reading or spoofing identity data.

const { embedUrl } = await volidator.generateEmbedToken({
actorId: session.userId,
expiresIn: '2h',
hostOrigin: 'https://app.yourcompany.com', // Parent origin
});

Install the Volidator React package in your parent web application to simplify the handshake logic.

Terminal window
npm install @volidator/react

Use the useVolidatorHydration hook on the page rendering the Volidator iframe. This hook automatically listens for resolution requests, deduplicates and caches IDs, batches requests, and communicates securely with the iframe.

import { useRef } from 'react';
import { useVolidatorHydration } from '@volidator/react';
export default function AuditLogsView({ embedUrl }: { embedUrl: string }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useVolidatorHydration({
iframeRef,
volidatorOrigin: 'https://dash.volidator.com',
resolveActors: async (ids) => {
// Call your backend API to map database IDs to display names
const res = await fetch('/api/users/resolve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
});
return res.json(); // returns Record<string, { name: string; avatarUrl?: string }>
},
});
return (
<iframe
ref={iframeRef}
src={embedUrl}
width="100%"
height="600"
style={{ border: 'none' }}
/>
);
}

On your backend, implement a batch endpoint that returns display names and optional avatars for the requested IDs.

// Next.js API Route Example (/pages/api/users/resolve.ts)
import { NextApiRequest, NextApiResponse } from 'next';
import { db } from '@/lib/db';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { ids } = req.body as { ids: string[] };
// Fetch only the requested users from your database
const users = await db.user.findMany({
where: { id: { in: ids } },
select: { id: true, name: true, image: true },
});
// Map to the required response structure
const mapping = users.reduce((acc, user) => {
acc[user.id] = {
name: user.name,
avatarUrl: user.image || undefined,
};
return acc;
}, {} as Record<string, { name: string; avatarUrl?: string }>);
return res.status(200).json(mapping);
}

To protect user privacy, both sides of the window handshake strictly enforce origins:

  • The parent window ignores messages from any origin other than the configured volidatorOrigin.
  • The embedded iframe ignores messages from any origin other than the hostOrigin specified in the signed embed token.

If an ID is not returned by the resolveActors callback (e.g., if a user was deleted from your database), the dashboard displays an amber warning badge: ⚠️ Unresolved: usr_890

The useVolidatorHydration hook caches resolved identities inside a React useRef cache. When pagination or search updates the logs table, only new, uncached reference IDs are requested from your backend API. This completely avoids the N+1 queries problem and eliminates redundant network traffic.