Where to store keys, how to rotate them, how to migrate from SQLCipher or unencrypted SQLite, and what SOC2, HIPAA, and PCI-DSS auditors actually want to see.

This is Part 3 of a three-part series on SQLite encryption. Part 1 covers the SQLite encryption options and how AEAD page-level encryption works. Part 2 covers Turso's two encryption models, working code, and benchmark numbers.
A developer spends a week building a well-architected encrypted database. Strong cipher. Properly generated 256-bit key. AEAD authentication on every page. The encryption is correct. Then the key ends up in a .env file that gets committed to a public GitHub repository. Or it shows up in plaintext in a Sentry exception log because the connection string includes it as a query parameter. Or it's hardcoded in a Docker image that gets pushed to a shared registry.
The encryption is technically perfect and operationally worthless.
This is the gap most encryption tutorials leave open. They show you how to generate a key and create an encrypted database, and then they stop. The hard part of encryption in production is not the cipher. It's the discipline around the key, the migration plan, the compliance story, and a clear understanding of what encryption actually protects.
Use a cryptographically secure random source. Nothing else.
For Turso Database (local/edge), keys are hex-encoded:
openssl rand -hex 32 # 256-bit key
openssl rand -hex 16 # 128-bit key
For Turso Cloud, keys are base64-encoded:
openssl rand -base64 32 # 256-bit key
openssl rand -base64 16 # 128-bit key
In application code:
# Python
import secrets
key_hex = secrets.token_hex(32) # 256-bit, hex (local)
// Node.js
const crypto = require("crypto");
const keyHex = crypto.randomBytes(32).toString("hex"); // local
const keyBase64 = crypto.randomBytes(32).toString("base64"); // cloud
The hex vs. base64 distinction between Turso Database and Turso Cloud is a real gotcha. If you generate a hex key and pass it to Turso Cloud (or vice versa), you'll get an error that doesn't immediately explain what went wrong. Local uses hex. Cloud uses base64. Tattoo it on your forearm if that helps.
Do not use Math.random(), Date.now(), UUIDs, or any deterministic source for key generation. These are not cryptographically secure. A key generated from Math.random() can be reproduced by anyone who knows the seed state, which defeats the entire purpose of encryption.
The right places:
.env files for local development only: Listed in .gitignore, never committed, never shared over Slack or email.The wrong places:
If you're using environment variables in production, make sure they're injected at runtime from a secret manager, not baked into the deployment artifact.
Native rekeying is on the roadmap for both Turso Database and Turso Cloud but is not yet available. The current workaround is export and reimport:
# Export with old key
turso db shell my-db \
--remote-encryption-key "${OLD_KEY}" .dump > export.sql
# Create new database with new key
turso db create my-db-rotated \
--remote-encryption-key "${NEW_KEY}" \
--remote-encryption-cipher aegis256
# Import into new database
turso db shell my-db-rotated \
--remote-encryption-key "${NEW_KEY}" < export.sql
# Validate, then cut over application traffic
This requires downtime. Know that before you design your rotation schedule. For most workloads, the downtime window is small (minutes for databases under a few gigabytes), but it's not zero. Plan accordingly.
When to rotate:
For SaaS applications with per-customer encryption, the pattern is:
This architecture means a single key compromise affects exactly one tenant. You can rotate individual tenant keys independently. And the mapping between tenants and keys lives in infrastructure designed for secret storage, not in your application's data layer.
Reasons to consider it: you're hitting the v3/v4 migration problem (different encryption parameters across major versions, databases that can't be opened after an upgrade), you want modern ciphers like AEGIS, you need cloud BYOK, or you're starting a new project and want to avoid version lock-in.
The migration is a decrypt-then-re-encrypt cycle:
# 1. Decrypt and export from SQLCipher
sqlcipher encrypted.db "PRAGMA key='YOUR_SQLCIPHER_KEY';" \
".output export.sql" ".dump"
# 2. Generate a new key
openssl rand -hex 32 > new_key.txt
# 3. Create encrypted Turso Database (local)
tursodb --experimental-encryption \
"file:migrated.db?cipher=aegis256&hexkey=$(cat new_key.txt)" < export.sql
(The .output step matters: PRAGMA key prints ok on success, and if you redirect stdout to the dump file instead, that ok lands at the top of your SQL and breaks the import.)
For Turso Cloud:
# Generate a base64 key for cloud
export NEW_KEY=$(openssl rand -base64 32)
# Create encrypted cloud database
turso db create migrated-db \
--remote-encryption-key "${NEW_KEY}" \
--remote-encryption-cipher aegis256
# Import
turso db shell migrated-db \
--remote-encryption-key "${NEW_KEY}" < export.sql
Test on a copy. Never run the migration against your production database directly. Keep the original SQLCipher database intact until you've fully validated the migration on production traffic.
The simpler case. Same pattern, fewer steps:
# Export
sqlite3 plaintext.db .dump > export.sql
# Create encrypted database (local)
tursodb --experimental-encryption \
"file:encrypted.db?cipher=aegis256&hexkey=YOUR_HEX_KEY" < export.sql
# Or create encrypted database (cloud)
turso db create encrypted-db \
--remote-encryption-key "YOUR_BASE64_KEY" \
--remote-encryption-cipher aegis256
turso db shell encrypted-db \
--remote-encryption-key "YOUR_BASE64_KEY" < export.sql
SEE databases use the standard SQLite file format when decrypted, so once you've decrypted with your SEE key active, the migration is identical to the unencrypted SQLite path. Export with your SEE CLI, then import into an encrypted Turso database.
One thing to factor into your testing: SEE is proprietary. You can't audit the decryption implementation without a license. Verify the exported data thoroughly before trusting it as your migration source.
Before cutting over production traffic:
We are SOC2 Type II certified. All Turso Cloud databases are encrypted at the volume level as part of that certification. BYOK is an additional layer on top: the customer generates and controls the encryption key, and Turso never stores it.
For your SOC2 audit, document:
The combination of Turso's SOC2 Type II infrastructure and customer-controlled BYOK keys gives you a layered story: the provider meets SOC2 standards, and the customer retains cryptographic control over their data.
The HIPAA Security Rule requires encryption of Protected Health Information (PHI) at rest. AEAD encryption satisfies this requirement, and the authentication component (the auth tag on every page) also provides integrity verification, which HIPAA requires separately.
Use AES-256-GCM for HIPAA workloads. Auditors know it. It's NIST-approved. AEGIS is a strong cipher with excellent performance properties, and it's in active IRTF review at the RFC Editor, but it is not yet a finalized standard and is not NIST-approved. Explaining AEGIS to a HIPAA auditor adds friction with no benefit. AES-256-GCM checks every box without a conversation.
With Turso Cloud BYOK, the covered entity controls the encryption keys. Turso operates as a business associate that never has access to PHI encryption keys. The key exists in Turso's infrastructure only in memory during query processing. This separation simplifies your Business Associate Agreement: include provisions covering Turso's SOC2 certification, the BYOK architecture, and the fact that the business associate never stores or has access to PHI encryption keys.
PCI-DSS requires NIST-approved encryption algorithms for cardholder data at rest. Same recommendation: use AES-256-GCM.
Document the full key management chain for your auditor:
BYOK means the payment processor or fintech platform retains control of cardholder data encryption keys. Turso never has access to the plaintext.
Across SOC2, HIPAA, and PCI-DSS, the documentation requirements converge on the same set of artifacts:
Build these artifacts before the audit, not during it.
AI agent architectures typically deploy fleets of agents, each handling private user data. Per-user encrypted databases give you cryptographic isolation: if one user's data is compromised, it's isolated from every other user's data by a different encryption key.
import { connect } from "@tursodatabase/database";
async function initializeAgent(userId: string, vault: SecretManager) {
const hexkey = await vault.getSecret(`encryption-key-${userId}`);
return connect(`agent-${userId}.db`, {
encryption: {
cipher: "aegis256",
hexkey,
},
});
}
The key comes from a vault lookup at runtime, not from an environment variable or config file. Each user's agent gets its own encrypted database in Turso Cloud. The encrypted database syncs to local embedded replicas for offline-capable agents. If the agent running for User A is compromised, User B's data is encrypted with a completely different key.
Use aes256gcm explicitly. BYOK means the payment processor never has access to cardholder data encryption keys. Turso never sees the plaintext.
turso db create payments-db \
--remote-encryption-key "${PAYMENT_KEY}" \
--remote-encryption-cipher aes256gcm
For auditors: we are SOC2 Type II certified, the customer controls the encryption key via BYOK, the key is never stored on our infrastructure, and AES-256-GCM is a NIST-approved cipher.
Same cipher recommendation: aes256gcm. The covered entity controls the keys. Turso as a business associate never sees PHI. AEAD provides both encryption (confidentiality) and integrity verification (the auth tag detects tampering), satisfying two HIPAA Security Rule requirements with one mechanism.
import { connect } from "@tursodatabase/database";
const patientDB = await connect("patient-records.db", {
encryption: {
cipher: "aes256gcm",
hexkey: process.env.PHI_ENCRYPTION_KEY,
},
});
For your Business Associate Agreement with Turso: include provisions covering the BYOK architecture, the fact that encryption keys are never stored by the business associate, and Turso's SOC2 Type II certification.
On a mobile device or edge node, encryption at rest protects data if the physical device is lost, stolen, or forensically analyzed. The overhead needs to be low enough that users don't notice it.
Turso Database encryption with AEGIS on ARM processors with AES extensions adds minimal overhead. The published benchmarks show 0.2% to 5.5% read overhead and around 13% write overhead with AEGIS-256. On modern iPhones and current Android flagships, that's negligible in practice.
For older devices without hardware AES support, two options: use aegis128l (faster than aegis256, lower overhead) or, if you're syncing with Turso Cloud, use chacha20poly1305 on the cloud side. ChaCha20-Poly1305 is designed to perform well in pure software without AES-NI instructions.
The offline-first pattern: an encrypted local Turso Database syncs to Turso Cloud when connectivity returns. Encryption is maintained end-to-end. The data is encrypted at rest on the device and encrypted at rest in the cloud, with TLS protecting it in transit between the two.
Encryption at rest protects against a specific threat model: unauthorized physical or filesystem access to storage media. A complete approach also includes parameterized queries, authentication and authorization, TLS for data in transit, secure key storage, and monitoring for anomalous access patterns.
Several features are in active development:
Native rekeying for both Turso Database and Turso Cloud will enable key rotation without the export/import workaround. This eliminates the downtime requirement for rotation and makes compliance-mandated rotation schedules practical at scale. It's on the roadmap. Not yet available.
Encrypting existing unencrypted databases in place will remove the dump/import step when adding encryption to a database that was originally created without it.
Key derivation from passphrases (KDF-based workflows) will support applications where passphrase-based key derivation is more appropriate than managing raw keys directly.
ATTACH statement support for encrypted databases will allow attaching multiple encrypted databases to a single connection, each potentially with different keys.
As AEGIS reaches RFC finalization and gains broader adoption, compliance frameworks may follow. But don't wait on that for current compliance requirements. AES-256-GCM is the right choice today for regulated workloads, and it will remain a strong choice regardless of what happens with AEGIS standardization.
Part 1 covers the SQLite encryption landscape: how page-level AEAD encryption works, and how SQLCipher, SEE, and newer alternatives compare. Start there if you're still evaluating your options.
Part 2 covers both encryption models end to end with working code, cipher selection guidance, and the benchmark numbers. Start there if you're ready to write code.
Resources