Field-level encryption
Field-level encryption encrypts individual database columns rather than the whole disk or table, so a leaked backup, replica, or table dump reveals only ciphertext for that field. Caisson's field-crypto package wraps this in a Drizzle customType: plaintext seals to AES-256-GCM on write and opens on read, transparent to every query the app writes.
In code
export function encryptedColumn(
columnContext: string,
cipher: AeadCipher = aesGcm,
) {
return customType<{ data: string; driverData: string }>({
dataType() {
return "text";
},
toDriver(plaintext: string): string {
return sealField(
currentFieldCryptoContext(),
columnContext,
plaintext,
cipher,
);
},
fromDriver(stored: string): string {
return openField(currentFieldCryptoContext(), columnContext, stored);
},
});
}How it holds
A Drizzle column type, not an app-code habit
encryptedColumn wraps a Postgres text column in a Drizzle customType: toDriver seals plaintext on write, fromDriver opens it on read. Every query that touches the column encrypts or decrypts automatically, so there is no separate encrypt-then-insert call to remember or forget.
AES-256-GCM, a fresh nonce every write
The cipher is AES-256-GCM through node:crypto's native binding: zero dependency, AES-NI accelerated. Every encrypt draws a new CSPRNG nonce, a (key, nonce) pair is never reused, and a tampered ciphertext or auth tag fails decryption outright rather than returning altered plaintext.
AAD locks a ciphertext to its tenant, column, and key version
buildAad binds tenant_id, key_version, and the column's stable identity into the AEAD's authenticated data. Move a cell to another tenant or another column and it fails to authenticate on decrypt even though the underlying bytes are unchanged: a cryptographic property, not an application check.
Fail-closed: no bound tenant, no encrypt or decrypt
encryptedColumn reads currentFieldCryptoContext() from an AsyncLocalStorage the caller binds via withFieldCryptoContext. A query that reaches an encrypted column outside that scope throws instead of encrypting or decrypting under a coerced or missing tenant.