Encryption setup
Use tenant data encryption to protect sensitive columns with per-tenant DEKs, keep deterministic hashes for lookups, and let admins choose which fields stay encrypted.
Environment switches
TENANT_DATA_ENCRYPTION–yes|no(defaultyes). Set tonoto run unencrypted (hooks no-op, validation stays).TENANT_DATA_ENCRYPTION_DEBUG–yesto log map evaluation, Vault calls, cache hits, and fallback selection.VAULT_ADDR,VAULT_TOKEN– required for HashiCorp Vault KMS. Example:https://vault.example.com.VAULT_KV_PATH– KV v2 mount for tenant keys (defaultsecret/data).- Fallback (dev / when Vault is down): set
TENANT_DATA_ENCRYPTION_FALLBACK_KEY(preferred) or legacyTENANT_DATA_ENCRYPTION_KEY.AUTH_SECRETandNEXTAUTH_SECRETare not used for data encryption. The built-in dev fallback is disabled unlessALLOW_DERIVED_KMS_FALLBACK=true; production without a dedicated fallback secret falls back to noop KMS.
Note: changing encryption maps or toggling the Encrypted flag on a custom field only applies to data written after the change; previously stored values stay as they were unless you re-save or migrate them.
Vector search embeddings (important)
When using the vector module (vector search / embeddings), be aware that embeddings are stored unencrypted (for example, in Postgres pgvector the raw vector is stored in vector_search.embedding). Even though the source text is decrypted only transiently to compute the embedding and result metadata is encrypted by default, embeddings can still indirectly encode information about the underlying text.
In practice, reconstructing the original text from embeddings is difficult, but treat embeddings as sensitive data:
- Avoid embedding raw PII/high-sensitivity text; redact or transform inputs in your module’s
buildSource. - Limit database access to the vector store and rely on disk-level / managed database encryption-at-rest where possible.
Vault setup (KMS)
- Enable KV v2 and pick a mount, e.g.
secret/:vault secrets enable -path=secret kv-v2
- Export envs for the backend:
VAULT_ADDR=https://vault.example.comVAULT_TOKEN=<token>VAULT_KV_PATH=secret/dataTENANT_DATA_ENCRYPTION=yes
- Start the app; DEKs are created per tenant at
secret/data/tenant_key_<tenantId>. - To pre-create a key manually (optional):
vault kv put secret/tenant_key_<tenantId> key=$(openssl rand -base64 32).
Define encryption maps in code
- Each owning module can declare default maps in a root
encryption.tsfile by exportingdefaultEncryptionMaps:
import type { ModuleEncryptionMap } from '@saasframe/shared/modules/encryption'
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
{
entityId: 'customers:customer_address',
fields: [
{ field: 'postal_code' },
{ field: 'email', hashField: 'email_hash' }, // add hash when lookups must stay deterministic
],
},
]
- Seed or update maps for a tenant with the CLI (respects the env toggle):
yarn saasframe entities seed-encryption --tenant <tenantId> [--organization <orgId>]
- During
auth:setup, the app collectsdefaultEncryptionMapsfrom enabled modules and applies them automatically when encryption is enabled.
Manage maps in the UI
Open Configuration → Encryption. Pick an entity, toggle Encryption enabled for this entity, and choose which fields are encrypted plus optional hash columns.

Notes:
- Maps are per tenant/organization; use field names from your entities.
- Hash fields let you keep deterministic lookups (e.g., login by email) while the main column is encrypted.
- Click Save encryption map to persist and invalidate caches.
Encrypt custom fields
When defining a custom field, check Encrypted to store its values with the tenant DEK. Works for text, multiline, integer, float, boolean, select, and relation kinds.

Key rotation and backfills
Use the CLI to encrypt plaintext rows or rotate a previous fallback key to the current KMS/fallback.
Auth users:
yarn saasframe auth rotate-encryption-key
yarn saasframe auth rotate-encryption-key --old-key <previous_fallback_key>
yarn saasframe auth rotate-encryption-key --tenant <tenantId> --org <organizationId>
All entities with encryption maps:
yarn saasframe entities rotate-encryption-key
yarn saasframe entities rotate-encryption-key --old-key <previous_fallback_key>
yarn saasframe entities rotate-encryption-key --tenant <tenantId> --org <organizationId>
Notes:
- These commands are potentially destructive; run a backup first and prefer
--dry-run. - Without
--old-key, commands only encrypt plaintext values and skip already encrypted fields. - With
--old-key, rotation only updates fields that can be decrypted with that key; mismatched rows are skipped and logged. - Always pass
--tenant(and--orgwhen possible) during rotation to avoid accidental cross-tenant use. - Rotation always re-encrypts with the current KMS/fallback key. Ensure the new key is already active (Vault configured or
TENANT_DATA_ENCRYPTION_FALLBACK_KEYset) before running these commands.
Decrypting data (removing encryption)
Use decrypt-database to write all encrypted fields back to plaintext for a tenant. This is irreversible — take a full backup first.
# Preview only (no writes)
yarn saasframe entities decrypt-database --tenant <tenantId> --confirm <tenantId> --dry-run
# Check encryption status and sampled payload estimate
yarn saasframe entities decrypt-database --tenant <tenantId> --check
# Full decryption
yarn saasframe entities decrypt-database --tenant <tenantId> --confirm <tenantId>
# Full decryption + deactivate encryption maps
yarn saasframe entities decrypt-database --tenant <tenantId> --confirm <tenantId> --deactivate-maps
After a successful run:
- Set
TENANT_DATA_ENCRYPTION=falsein your environment / secrets. - Restart all application replicas to flush in-process encryption caches.
- Run
yarn saasframe query_index reindex --tenant <tenantId>to rebuild search indexes. - Run
--checkagain to confirm no encrypted values remain.
The --confirm <tenantUuid> flag is a required safety gate — it must exactly match --tenant to prevent accidental cross-tenant runs. See the decrypt-database CLI reference for the full option list, error codes, and troubleshooting.
Debugging
- Turn on
TENANT_DATA_ENCRYPTION_DEBUG=yesto see KMS cache hits/misses, Vault calls, and which fields were encrypted. - Debug logs also call out Vault health and the selected KMS path (Vault vs derived vs noop) so you can verify
VAULT_ADDR,VAULT_TOKEN, andVAULT_KV_PATHwithout exposing the token value. - If Vault is unreachable, the runtime logs a warning and falls back to the derived-key KMS when a dedicated fallback secret is present; otherwise it becomes a noop KMS (data stays plaintext). Integration credentials fail closed instead of saving under a noop or auth-derived key.
- Use Configuration → System status to confirm the active values of the encryption env vars in the running instance.
Running without Vault
If you don’t want to depend on Vault, set a fallback secret and the runtime will derive tenant keys from it:
TENANT_DATA_ENCRYPTION=yesTENANT_DATA_ENCRYPTION_FALLBACK_KEY=<32+ char secret>(preferred) orTENANT_DATA_ENCRYPTION_KEY=<secret>
When Vault is down or not configured, a warning banner appears and shows which env var supplied the derived key. Avoid short/shared secrets—treat this like any other encryption root and store it securely.