Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.lintliot.com/llms.txt

Use this file to discover all available pages before exploring further.

The Vault adds field-level AES-256-GCM encryption to your application without changing your data model or your ORM queries. You call lintliot.vault.encrypt() before writing to your database, lintliot.vault.decrypt() after reading, and LintLiot handles key management, key versioning, and rotation. Your database stores ciphertext; only your application — with a valid LintLiot API key — can read the plaintext. Field-level encryption satisfies the encryption-at-rest requirements of GDPR (Art. 32), HIPAA (§164.312(a)(2)(iv)), and PCI-DSS v4.0 (Req. 3.5). Enabling the Vault automatically advances your compliance score for those controls.

How Vault encryption works

Each encryption operation generates a fresh 12-byte random IV. The ciphertext includes the IV, a 128-bit authentication tag, the key version number, and optionally a context string that binds the ciphertext to a specific record:
_lk_enc:v{version}:{ivHex}:{authTagHex}:{ciphertextHex}[:{context}]
The authentication tag means the ciphertext is tamper-proof: if any byte of the stored value is modified, decryption fails. The context string (Additional Authenticated Data) prevents ciphertext transplanting — an encrypted email address from one user record cannot be moved to another user’s row and decrypted, because the context (typically the user ID) won’t match. Encryption keys are fetched from LintLiot’s key management service and cached for 5 minutes. If the key service is unreachable, the most recent cached key is used. If no cached key is available and the service is down, the data is written in plaintext and a high-severity vault.encrypt_failed event is logged.

Encrypting fields before a database write

Pass your data object and specify which fields to encrypt. Any field not in the list is passed through unchanged:
import { createLintliot } from '@lintliot/sdk'

const lintliot = createLintliot({ apiKey: process.env.LINTLIOT_API_KEY })

// In your user creation handler
async function createUser(userData: {
  name: string
  email: string
  phone: string
  dateOfBirth: string
}) {
  // Encrypt PII fields before storing
  const safeUser = await lintliot.vault.encrypt(userData, {
    fields: ['email', 'phone', 'dateOfBirth'],
    context: userData.name, // ties ciphertext to this record
  })

  // safeUser.email is now "_lk_enc:v1:a3f2...":... instead of "alice@example.com"
  // safeUser.name is unchanged
  await db.users.create({ data: safeUser })
}

Decrypting fields after a database read

async function getUser(userId: string) {
  const raw = await db.users.findUnique({ where: { id: userId } })
  if (!raw) return null

  // Decrypt all encrypted fields
  const user = await lintliot.vault.decrypt(raw)
  // user.email is now "alice@example.com" again

  return user
}
You can also decrypt only specific fields if you don’t want to expose all PII in the same operation:
// Only decrypt what you need for this operation
const user = await lintliot.vault.decrypt(raw, {
  fields: ['email'], // phone and dateOfBirth stay encrypted
})

Batch operations

For queries that return multiple records, use encryptBatch and decryptBatch — they fetch the key once and process all records in a single pass:
// Encrypt before bulk insert
const safeUsers = await lintliot.vault.encryptBatch(users, {
  fields: ['email', 'phone'],
})
await db.users.createMany({ data: safeUsers })

// Decrypt after bulk read
const rawUsers = await db.users.findMany()
const users = await lintliot.vault.decryptBatch(rawUsers)

Database proxy (auto-encrypt/decrypt)

For the most seamless integration, wrap your database client with withEncryption(). All writes are encrypted and all reads are decrypted automatically, without changing any of your query code:
import { createClient } from '@supabase/supabase-js'
import { createLintliot } from '@lintliot/sdk'

const lintliot = createLintliot({ apiKey: process.env.LINTLIOT_API_KEY })
const supabaseRaw = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)

// Wrap the Supabase client
const db = lintliot.vault.withEncryption(supabaseRaw, {
  fields: ['email', 'phone', 'ssn', 'dateOfBirth'],
})

// From this point on, use db exactly as you would use supabaseRaw
// Writes are automatically encrypted; reads are automatically decrypted

await db.from('users').insert({
  name: 'Alice',
  email: 'alice@example.com', // stored as "_lk_enc:v1:..."
})

const { data } = await db.from('users').select().eq('id', userId)
// data[0].email === 'alice@example.com' — already decrypted
The proxy intercepts write methods (insert, upsert, update, create, save) and read methods (select, findFirst, findMany, get, query). Delete, count, and aggregate calls pass through unmodified.
The proxy works with any ORM or database client that uses an object-of-table-clients shape: Prisma, Drizzle, Supabase JS, Kysely, and others. If you’re using a client with a different shape, use encrypt() and decrypt() directly.

EdgeVault for Edge runtimes

The main Vault uses Node.js’s crypto module. If your application runs in an Edge runtime (Next.js middleware, Cloudflare Workers, Vercel Edge Functions), import EdgeVault instead. It uses the Web Crypto API (SubtleCrypto) and produces ciphertext in the same format — encrypted values are portable between Node.js and Edge environments.
// In Next.js middleware or any Edge runtime
import { EdgeVault } from '@lintliot/sdk/edge'

const vault = new EdgeVault({ apiKey: process.env.LINTLIOT_API_KEY })

export async function middleware(request: Request) {
  // Decrypt user data in Edge middleware
  const userData = await getSessionData(request)
  const decrypted = await vault.decrypt(userData)

  // ... continue with decrypted data
}
For one-off encryption without a Vault instance (useful in Edge API routes):
import { edgeEncrypt, edgeDecrypt } from '@lintliot/sdk/edge'

// Encrypt a single value using a raw hex key
const encryptedEmail = await edgeEncrypt(
  'alice@example.com',
  process.env.VAULT_KEY!, // 64-char hex string (32 bytes)
  userId, // optional context
)

// Decrypt it later
const email = await edgeDecrypt(encryptedEmail, process.env.VAULT_KEY!)

Default sensitive fields

If you call encrypt() without specifying fields, LintLiot automatically encrypts any fields whose names match a built-in list of well-known PII field names:
email, phone, phoneNumber, ssn, socialSecurityNumber, dob, dateOfBirth,
address, street, zipCode, postalCode, creditCard, cardNumber, cvv,
passwordHash, ipAddress, passport, driversLicense, bankAccount,
routingNumber, taxId, nationalId
This means you can add encryption to an existing application with a single line of code and immediately protect common PII fields without auditing your schema.

Discovering unencrypted PII

Use scanForUnencrypted() to audit an existing object and find fields that look like PII but aren’t encrypted yet:
const rawUser = await db.users.findFirst({ where: { id: userId } })

const unprotected = lintliot.vault.scanForUnencrypted(rawUser)
// Returns: ['email', 'phone'] if those fields contain plaintext values

if (unprotected.length > 0) {
  console.warn('Unencrypted PII fields found:', unprotected)
}
This is useful for a one-time audit of existing data before enabling encryption, or as a CI check to prevent unencrypted PII from slipping into new code paths.

Key rotation

LintLiot manages key rotation with zero downtime. When you rotate your encryption key from the dashboard:
  1. A new key version is generated and marked active
  2. New encryptions use the new key; the key version is embedded in each ciphertext
  3. Old ciphertexts (tagged with the previous version number) continue to decrypt with the old key until they’re re-encrypted
  4. You can trigger a background re-encryption job from the dashboard to migrate existing ciphertexts to the new key
Your application code doesn’t change during or after rotation — the Vault handles key version selection automatically.
Key rotation invalidates all ciphertexts encrypted with the old key once the migration is complete. Run the re-encryption job before retiring an old key version. If you retire a key version before migrating all ciphertexts, those records will fail to decrypt.