Skip to content

Customer Portals & Multi-Tenancy

When building B2B SaaS applications, a common requirement is to display audit logs to your end-customers (e.g., let “John” or “John’s Bakery” see their own logs).

Volidator makes this possible while preserving its Zero-Knowledge model. Your platform can scope logs to a specific tenant, a specific resource target, or a single user. Decryption occurs entirely in the customer’s browser. Volidator’s servers never see who the tenant is, nor what actions they performed.


To support multi-tenant dashboards, Volidator allows you to:

  1. Tag logs at ingestion time with a tenant and target.
  2. Deterministic Hashing: The SDK hashes these identifiers into tenantBlindIndex and targetBlindIndex locally before transmitting them to Volidator’s API.
  3. Scope Tokens: You generate short-lived, signed JWT tokens specifying the query scope (tenant, actor, target, or all).
  4. Edge Filtering & Browser Decryption: Volidator queries the database using only the hashed blind indexes and sends the encrypted payloads to the browser, where the embedded widget decrypts and displays them.

When tracking events, specify the tenant (e.g. organization name/ID) and target (e.g. resource ID/name) in your log() calls. The SDK supports both raw strings and ReferencePayload objects (for JIT Hydration).

// 1. Example: Alice at John's Bakery updates the payment gateway
await volidator.log({
action: "gateway.update",
target: "payment-gateway-config",
tenant: "johnsbakery", // Scopes the log to the organization
});
// 2. Example: Bob at John's Bakery logs in
await volidator.log({
action: "user.login",
target: "dashboard",
tenant: "johnsbakery",
});
// 3. Example: Platform admin modifies Alice's profile (no tenant)
await volidator.log({
action: "profile.update",
target: "[email protected]",
});

In a server-side route of your application, generate the signed embed URL. Specify the identifier parameters (actorId, targetId, tenantId) and a query scope determining what the user is allowed to view.

const { embedUrl } = await volidator.generateEmbedToken({
tenantId: "johnsbakery", // Scopes query to this tenant identifier
scope: "tenant", // Filters only logs matching tenantId
expiresIn: "30m", // Capped at 1 hour for security
});
Scope Description Required Parameters Matches Logs Where
tenant Standard B2B multi-tenancy. Returns all logs for a workspace/client. tenantId tenantBlindIndex === HMAC(tenantId)
actor Displays logs of a specific individual user (e.g. “John”). actorId actorBlindIndex === HMAC(actorId)
target Displays logs affecting a specific resource (e.g. “Invoice-99”). targetId targetBlindIndex === HMAC(targetId)
all Combined broad access. At least one of the above actor, target OR tenant matches any provided IDs

Pass the generated embedUrl to your frontend and render it inside an <iframe>.

// Next.js (App Router) example
export default async function TenantLogsPage() {
const { embedUrl } = await volidator.generateEmbedToken({
tenantId: "johnsbakery",
scope: "tenant",
expiresIn: "30m",
});
return (
<div className="logs-container">
<h1>Audit Logs</h1>
<iframe
src={embedUrl}
width="100%"
height="600"
style={{ border: 'none', borderRadius: '12px' }}
title="Audit Logs Portal"
/>
</div>
);
}

Key Rotation (Zero-Knowledge Keyring Pattern)

Section titled “Key Rotation (Zero-Knowledge Keyring Pattern)”

When security guidelines require rotating your client Data Encryption Keys (DEKs), doing so blindly can break decryption and searchability of historical logs (as they were encrypted and hashed under a different key).

To prevent data accessibility loss, Volidator supports a Keyring Model allowing you to retain historical keys for reading while defining a single key for writing new logs.

Instead of a single encryption key string, pass the keyring mapping and specify the activeEncryptionKeyId. This ensures all new logs are encrypted and indexed using the active key version, while past logs remain decryptable.

const volidator = new VolidatorClient({
apiKey: process.env.VOLIDATOR_API_KEY!,
// Specifies which key version to use for new incoming logs
activeEncryptionKeyId: "v2",
// List of active and historical keys for decryption & querying
keyring: {
"v2": "current-encryption-key-entropy...",
"v1": "historical-encryption-key-entropy..."
}
});

When generating embed tokens or querying logs, the SDK automatically hashes your search parameter (e.g. tenantId: "johnsbakery") against all keys in your keyring.

Because the SDK sends all derived blind indexes within the secure scoped token, the Volidator API can query logs matching any of these computed blind indexes. This guarantees that your historical logs (encrypted under older keys) are still retrieved alongside newer logs, all without the Volidator platform ever knowing the underlying plaintext values of your keys or query parameters.


  1. Short Expirations: Embed tokens are processed by the end-user’s browser. To prevent exfiltration or replay attacks, the SDK automatically caps the token’s lifetime (expiresIn) at a maximum of 1 hour. Keep this value relatively low (e.g. 15m to 30m) and regenerate the token on each page load.
  2. Restricting Origins: Always pass hostOrigin when generating embed tokens. This locks down postMessage communication and JIT hydration handshake to only run on your approved frontend origin (e.g., https://app.yourdomain.com).
  3. Keyring Limits: Limit your keyring to a maximum of 5 keys. Exceeding this limit will inflate the SQL search parameters and degrade client-side performance. For standard rotation guidelines, a keyring of 5 keys is sufficient to cover rolling histories (e.g. quarterly rotations over a year).