Secured
Guides

Vaults

Configure Secured's entity storage system so global entities, thread entities, and runtime aliases are reused consistently.

Overview

The vault subsystem is the implementation behind Secured's entity storage system. It lets the SDK reuse known replacements instead of generating new ones every time.

At runtime, a vault can combine:

  • a global encrypted snapshot from your backend
  • optional thread-level encrypted snapshots
  • SDK-managed runtime thread entries
  • pending entries created before a thread is known

This gives you stable replacements such as keeping the same project codename or person alias across a thread, workspace, or product surface.

If your team talks about "global entities" or "stored entities", this is the system they belong to. In the current code, those stored entities are represented as VaultEntry records and managed by VaultManager.

Stored entity shape

Every stored entity record has the same shape after decryption:

interface VaultEntry {
  original: string
  replacement: string
  type: string
}

Example:

{
  original: 'ProjectFalcon',
  replacement: 'PROJECT_TOKEN',
  type: 'CUSTOM',
}

Global entities vs thread entities

Global entities

Global entities are workspace-level or product-level mappings fetched from your backend during initialization. They are the default persisted store for canonical replacements.

Thread entities

Thread entities are persisted mappings for one specific thread, conversation, or case. They override global entities when a thread context is active.

Runtime thread entities

If the SDK generates a new replacement while a thread is active, it stores that generated entity locally as a runtime thread entry so future obfuscation in that thread reuses the same alias.

Pending entities

If the SDK generates a new replacement before any thread is active, it stores it as a pending entity and later migrates it into the first thread you activate.

Configure a vault

import { PrivacyClient } from '@secured-ai/core'

const client = new PrivacyClient({
  baseUrl: 'https://dev-api.securedai.com',
  sdkAccessToken: import.meta.env.VITE_SECURED_SDK_ACCESS_TOKEN,
  vault: {
    repository: {
      getGlobalSnapshot: async () => {
        return null
      },
      getThreadSnapshot: async (threadId) => {
        return null
      },
    },
    masterKey: async () => 'vault-master-key',
  },
})

PrivacyVaultConfig

Prop

Type

masterKey and cacheNamespace can be async runtime values, which is helpful when auth or workspace context is not available at construction time.

Master keys

masterKey is the secret the client uses to decrypt persisted entity snapshots after they are fetched from your repository.

In practice:

  • your backend returns encrypted global or thread snapshots
  • the SDK fetches those snapshots
  • the vault crypto layer decrypts them with masterKey
  • the decrypted VaultEntry[] records become available to replacement resolution

You can provide masterKey as:

  • a plain string if it is already available
  • an async runtime getter if it only becomes available after unlock, auth, or workspace selection
const client = new PrivacyClient({
  baseUrl: 'https://dev-api.securedai.com',
  sdkAccessToken: import.meta.env.VITE_SECURED_SDK_ACCESS_TOKEN,
  vault: {
    repository,
    masterKey: async () => currentMasterKey,
  },
})

When the master key is not available yet

If masterKey resolves to null or is otherwise unavailable when the vault refreshes, persisted encrypted snapshots cannot be decrypted yet.

That means:

  • global or thread entities may not be loaded into usable VaultEntry records
  • the SDK falls back to generated replacements when needed
  • once the key becomes available, refreshVaultContext() should be called so snapshots can be retried and decrypted
await client.refreshVaultContext()

Security model

The repository does not return plaintext entity mappings in the normal persisted flow. It returns encrypted snapshot payloads, and the client-side crypto layer decrypts them with masterKey.

The default crypto implementation is WebCryptoVaultCrypto, which uses browser Web Crypto APIs to encrypt and decrypt stored entries.

Rotating or rebinding master keys

If the active master key can change at runtime, provide it as a runtime value and call refreshVaultContext() after the change. This is the expected path when:

  • a user unlocks the app and the key becomes available
  • the active workspace changes
  • the key depends on tenant-specific context

Repository contract

interface VaultRepository {
  getGlobalSnapshot(
    context: { cacheNamespace: string | null },
  ): Promise<EncryptedVaultSnapshot | null>

  getThreadSnapshot?(
    threadId: string,
    context: { cacheNamespace: string | null },
  ): Promise<EncryptedVaultSnapshot | null>
}

Snapshots are encrypted before they reach the client. The client decrypts them with the configured masterKey.

Default backend endpoints

If you use DefaultVaultRepository, it fetches persisted entity snapshots from:

  • GET /ai/sensitive-keys for global entities
  • GET /ai/threads/:threadId/sensitive-keys for thread entities

These responses are expected to contain encrypted snapshot payloads, not plaintext entities.

DefaultVaultRepository uses the baseUrl configured on PrivacyClient; do not configure a second backend URL on the repository itself.

Replacement precedence

When obfuscate() needs a replacement, the vault checks the most specific layer first:

When a thread is active:

  1. runtime thread entries
  2. persisted thread snapshot
  3. global snapshot
  4. generated replacement pool fallback

When no thread is active:

  1. pending entries created before a thread is known
  2. global snapshot
  3. generated replacement pool fallback

This means thread-local consistency wins over global aliases, while pending entities only participate before a thread context exists.

How matching works

Stored entity matching is normalization-based rather than raw string label equality.

The system normalizes:

  • original text by trimming and lowercasing
  • entity types through canonical aliases such as PER -> PERSON, LOC -> GPE, and ORGANIZATION -> ORG

That lets persisted entities continue matching even when upstream detectors use slightly different but equivalent type labels.

Thread-aware flows

If your app has a conversation or case ID, set it before obfuscating:

await client.setThreadContext('thread-42')
const result = await client.obfuscate('ProjectFalcon launches tomorrow')

New mappings created in that context become runtime thread entries and are reused for later obfuscation in the same thread.

If you obfuscate before a thread exists, entries are stored as pending and migrated into the first thread you set.

You can also scope a single obfuscation call without changing the globally active thread:

const result = await client.obfuscate('ProjectFalcon launches tomorrow', {
  threadId: 'thread-42',
})

Refreshing context

When auth, namespace, or workspace state changes, refresh the vault context:

await client.refreshVaultContext()

This re-resolves runtime values like masterKey and cacheNamespace and reloads snapshots.

This is especially important when:

  • the user has just unlocked the app and the master key becomes available
  • the active workspace changed
  • your cacheNamespace depends on tenant or workspace context

Inspecting vault state

The vault manager is available as client.vault.

const status = client.vault.getStatus()
const globalSnapshot = client.vault.getGlobalSnapshot()
const threadSnapshot = await client.vault.loadThreadSnapshot('thread-42')

Helpful methods include:

  • getStatus()
  • getGlobalSnapshot()
  • getThreadSnapshot(threadId)
  • loadThreadSnapshot(threadId)
  • getRuntimeThreadEntries(threadId)
  • getPendingEntries()
  • clearCache()
  • deobfuscate(text, options?)

getStatus() is useful for checking whether global entities are loaded, whether the current namespace has been resolved, and whether the cached global snapshot is stale.

Cache and namespace behavior

Vault snapshots are cached client-side. If you provide cacheNamespace, the cache is partitioned by that namespace so different workspaces do not bleed into each other.

If snapshot fetching fails but a cached copy exists, the vault can continue from cached data and mark it as stale.

Choosing a cache adapter

Secured exposes multiple cache adapters, and the right one depends on how durable you want entity storage to be:

  • MemoryVaultCache for ephemeral in-memory usage and tests
  • LocalStorageVaultCache for persistence across reloads in the same browser profile
  • SessionStorageVaultCache for per-tab persistence that disappears when the tab/session closes
  • IndexedDbVaultCache for more durable async browser storage

If you do not provide a cache explicitly, the vault defaults to MemoryVaultCache.

import {
  IndexedDbVaultCache,
  LocalStorageVaultCache,
  MemoryVaultCache,
  SessionStorageVaultCache,
} from '@secured-ai/core'

React integration

@secured-ai/react exposes this through useVault(), including helpers for setThreadContext() and refreshContext().

On this page