Secured
Concepts

Entity Storage

How Secured stores and reuses global entities, thread entities, and runtime-generated aliases.

Overview

Secured has a storage layer for previously known sensitive entities and their approved replacements. In the codebase this is implemented by the vault subsystem, but conceptually it is the library's entity storage system.

That system exists so the library can answer questions like:

  • "Have we already assigned a global alias for this entity?"
  • "Does this thread already know the replacement for this value?"
  • "If we generated a new alias a moment ago, should we reuse it on the next obfuscation call?"

Without this layer, obfuscation would only use replacement pools and would generate fresh stand-in values more often.

The four storage layers

Secured can combine four different layers of stored entities:

  1. Global entities Shared encrypted entity mappings fetched from your backend for the whole workspace or product surface.
  2. Thread entities Encrypted mappings scoped to a specific thread, conversation, or case.
  3. Runtime thread entities Mappings generated by the SDK during the current thread lifecycle and cached locally.
  4. Pending entities Mappings generated before a thread is known, waiting to be attached to the first thread context.

All four layers use the same logical shape:

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

Why "global entities" matter

Global entities are the part most likely to feel "new" at the product level. They let your backend publish a canonical replacement for important values so the frontend SDK can reuse them during obfuscation.

Example:

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

Once this exists in the global entity store, any later obfuscation of ProjectFalcon can reuse PROJECT_TOKEN instead of inventing a new alias.

Precedence rules

When the library tries to resolve a replacement, it checks the most specific stored layer first.

If a thread context is active:

  1. runtime thread entities
  2. persisted thread entities
  3. global entities
  4. generated replacement fallback

If no thread context is active:

  1. pending entities
  2. global entities
  3. generated replacement fallback

This is why a thread-specific alias can override a workspace-level alias.

How entities are matched

Stored entities are matched by normalized original text plus normalized type.

That means the storage system canonicalizes entity types so equivalent labels can still match:

  • PER, NAME, PERSON_NAME -> PERSON
  • LOC, LOCATION -> GPE
  • ORGANIZATION -> ORG

So a stored global entity with type LOC can still satisfy a detected entity of type GPE.

What gets persisted by your backend vs by the SDK

Your backend is responsible for persisted global and optionally persisted thread entities.

The SDK is responsible for:

  • decrypting persisted snapshots
  • reading and caching them client-side
  • generating new runtime entities when no stored match exists
  • reusing those runtime entities on later obfuscation calls
  • migrating pending entities into a thread when one is later selected

Backend snapshot model

The default repository expects encrypted snapshots rather than plaintext entries.

Global entities are fetched from:

GET /ai/sensitive-keys

Thread entities are fetched from:

GET /ai/threads/:threadId/sensitive-keys

The client decrypts the encrypted payload with the configured masterKey, then exposes the decrypted entries through the vault manager.

Lazy loading behavior

Global entities are loaded during vault initialization.

Thread entities are loaded lazily:

  • either when you call setThreadContext(threadId)
  • or when you pass threadId directly to obfuscate(..., { threadId })
  • or when you explicitly call client.vault.loadThreadSnapshot(threadId)

This keeps thread-specific fetches off the critical path until they are actually needed.

Runtime-generated entities

If no stored entity exists, the client generates a replacement from the configured replacement pools and immediately remembers it in local runtime storage.

That means the next obfuscation call can reuse the same replacement instead of generating a new one.

This behavior is what gives Secured stable aliases even before the backend has persisted anything.

Pending entities

If obfuscation happens before a thread is known, generated entities are temporarily stored as pending.

Once you later call setThreadContext('thread-123'), those pending entities are migrated into that thread's runtime entity layer and stop living in the pending bucket.

Cache namespaces

Entity storage can be namespaced with cacheNamespace, which isolates cached snapshots for different workspaces or tenants.

This prevents one workspace's global entities from bleeding into another workspace's client cache.

Where to configure it

The entity storage system is configured through PrivacyClientConfig.vault.

See:

On this page