How Turso's two encryption models work in practice: native encryption in the Rust engine and BYOK on Turso Cloud, with working code and real benchmark numbers.

This is Part 2 of a series on SQLite encryption. Part 1 covers the broader SQLite encryption options and how AEAD page-level encryption works.
Most SQLite encryption tutorials follow the same script. Generate a key. Set a PRAGMA. Open the database. Done. Encryption becomes a configuration detail, something that happens behind a function call you don't need to think about.
That framing hides the questions that actually matter. Who holds the key? Is the encryption implementation auditable? What happens if the provider's storage is breached? Can you prove to an auditor that the database vendor never had access to plaintext?
Our approach to SQLite encryption is different from the extension-based solutions covered in Part 1, and it's different in two specific ways worth understanding before you write a line of code.
First, encryption in Turso Database is native to the Rust engine. Not an extension bolted onto SQLite's C codebase via a hook API. The encryption code lives in the same Rust codebase as the rest of the database, it's open source under the MIT license, and you can read every line of it. This is a meaningful difference from SEE (proprietary, no source access without a $2,000 license) and from the extension-based architecture that SQLCipher and SQLite3MultipleCiphers use.
Second, Turso Cloud's BYOK (Bring Your Own Key) model is designed so that Turso never stores your encryption key. You supply the key per connection. It exists in memory only while your query is being processed. It's never written to disk, never included in logs, never persisted to any storage layer. The practical implication: if Turso's storage infrastructure is compromised, an attacker gets ciphertext and no keys.
That's the architecture. This article shows how it actually works, with code you can run, benchmark numbers you can reference, and clear guidance on when each model is the right fit.
Turso offers two distinct encryption models. They serve different deployment contexts, use different key formats, and have different maturity levels. Conflating them is a real source of confusion, so let's separate them clearly.
Turso Database is the local and edge engine. It's a complete rewrite of SQLite in Rust, open source under the MIT license. Encryption requires the --experimental-encryption flag. Keys are hex-encoded. This is the right choice for local apps, offline-first architectures, development environments, and edge deployments where the database lives on hardware you control (or don't).
Turso Cloud BYOK is the cloud-hosted offering. Encryption is production-ready on Pro plan and above (self-service availability since February 2026, previously Enterprise-only). Keys are base64-encoded. Turso never stores the key. This is the right choice for cloud-deployed applications, multi-tenant SaaS, and compliance-sensitive workloads.
The key format difference trips people up. Turso Database uses hex: openssl rand -hex 32. Turso Cloud uses base64: openssl rand -base64 32. If you generate a key in the wrong format, you'll get an error that doesn't immediately tell you what went wrong. Keep this distinction in mind as you read the sections below.
Encryption in Turso Database is implemented in Rust within the database engine itself. It's not a separate extension loaded at runtime, and it doesn't depend on a hook API that could be removed (as happened with SQLITE_HAS_CODEC in 2020). The encryption code is part of the same codebase that handles page I/O, which means it's integrated at the level where pages are read from and written to disk.
The entire codebase is MIT-licensed and on GitHub. If your security team wants to audit the encryption implementation, they can. That's a concrete advantage over proprietary options like SEE, where you need to buy a license before you can see the source.
The --experimental-encryption flag is required. It signals that this feature has not yet been through a formal third-party security audit. The implementation works, the benchmarks are published, and the code is open source and readable. We're being straightforward about where things stand in the maturity cycle.
For development, local apps, prototyping, and non-critical workloads, it's a solid option. For production deployments handling sensitive data, Turso Cloud BYOK is the production-ready path until the experimental designation is lifted. If you're in a compliance review, your auditor will likely ask the same question.
Generate a 256-bit key in hex format:
openssl rand -hex 32
Store this key securely. Do not hardcode it. Do not commit it to version control.
Create a new encrypted database:
tursodb --experimental-encryption \
"file:myapp.db?cipher=aegis256&hexkey=YOUR_HEX_KEY"
Open an existing encrypted database with the same syntax:
tursodb --experimental-encryption \
"file:myapp.db?cipher=aegis256&hexkey=YOUR_HEX_KEY"
From here, it's just SQL. Encryption is transparent to your queries:
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);
INSERT INTO users VALUES (1, 'Alice', 'alice@example.com');
SELECT * FROM users;
Every page of the database file is encrypted on disk. The WAL file is encrypted too. If someone copies the file without the key, they get random-looking bytes. The key exists only in memory during the session and must be provided each time you open the database. Open the same file with the wrong key and you get an explicit decryption error, not garbage data. That's the AEAD authentication tag doing its job.
Turso Database supports two cipher families locally:
AEGIS (recommended for most workloads): aegis256 is the default recommendation, with a 256-bit key and 256-bit nonce. aegis128l uses a 128-bit key and offers faster throughput. The parallel variants (aegis256x2, aegis256x4, aegis128x2, aegis128x4) are optimized for CPUs with 256-bit or 512-bit SIMD registers.
AES-GCM (for compliance requirements): aes256gcm and aes128gcm are NIST-approved and recognized by compliance frameworks. If your auditor needs to check a box that says "NIST-approved cipher," these are the options.
No ChaCha20-Poly1305 support in the local engine. That cipher is available only in Turso Cloud.
We published benchmarks in the encryption announcement using AEGIS-256 with a 75/25 read/write split across varying database sizes. The headline numbers:
Mixed workload total overhead: 0.5% to 2.8%
Read overhead: 0.2% at 100K rows, scaling to 5.5% at 15M rows
Write overhead: approximately 9% to 14% across database sizes
The full benchmark table with methodology is in the announcement post. The benchmark code is publicly available if you want to run it on your own hardware.
For most applications, this overhead is not the bottleneck. A mobile app doing CRUD operations won't notice. An API server will see the encryption cost absorbed by network latency and application logic. Even batch processing with large databases shows less than 3% total time increase on mixed workloads. Write-heavy operations carry the highest relative cost (up to ~14%), but in absolute terms, we're talking about microseconds per operation.
All Turso Cloud databases are already encrypted at the volume level as part of SOC2 Type II compliance. BYOK is an additional layer on top of that, and the critical difference is who controls the key.
With volume-level encryption, Turso manages the keys. With BYOK, you generate the key, you store it in your secret manager, and you supply it per connection. Turso never stores it. This is verifiable from the architecture: the key is passed as a connection parameter, held in memory during query processing, and discarded when the connection closes. It's never written to disk on Turso's infrastructure.
BYOK encryption is available on Pro plan and above for self-service customers as of February 2026. Previously, it required an Enterprise plan.
Generate a key in base64 format (not hex, this is Turso Cloud):
openssl rand -base64 32
Create an encrypted cloud database:
turso db create my-encrypted-db \
--remote-encryption-key "YOUR_BASE64_KEY" \
--remote-encryption-cipher aegis256
If your account has more than one database group, add --group <name> to pick where it lives.
Connect with the key:
turso db shell my-encrypted-db \
--remote-encryption-key "YOUR_BASE64_KEY"
Try connecting without the key and you get encrypted data. No plaintext access.
This is the code most of you will actually use. In TypeScript/JavaScript with the @tursodatabase/serverless driver:
import { connect } from "@tursodatabase/serverless";
const conn = connect({
url: "https://my-encrypted-db-myorg.turso.io",
authToken: process.env.TURSO_AUTH_TOKEN,
remoteEncryptionKey: process.env.TURSO_ENCRYPTION_KEY,
});
// Encryption is transparent. Use it like any database connection.
const stmt = await conn.prepare("SELECT * FROM users WHERE id = ?");
const user = await stmt.get([userId]);
The remoteEncryptionKey field is all that's needed. The cipher is not part of the connection: it was fixed when the database was created. The key comes from your environment variables, your vault, your secret manager. Not from Turso.
Turso Cloud supports three cipher families:
AEGIS: aegis256, aegis128l (recommended for performance)
AES-GCM: aes256gcm, aes128gcm (recommended for compliance)
ChaCha20-Poly1305: chacha20poly1305 (software-optimized, for environments without hardware AES support)
ChaCha20-Poly1305 is only available in Turso Cloud. If you need it locally, it's not an option in Turso Database today.
Encrypted databases in Turso Cloud support branching, point-in-time recovery, and sync to embedded replicas. One thing to know about branching: when you create a branch of an encrypted database, you must supply the same encryption key used for the parent. The branch inherits the cipher and key. You can't branch with a different key.
This is where Turso's architecture enables something that's genuinely hard to do with other SQLite encryption solutions.
Because Turso Cloud supports creating an effectively unlimited number of individual databases with no meaningful per-database overhead, you can provision one encrypted database per customer, each with its own unique key. This is not row-level access control, which depends on application logic and can be bypassed with sufficient privileges. It's not a shared database with per-tenant key management layered on top. It's actual cryptographic isolation: separate databases, separate keys, separate ciphertext.
If Customer A's key is compromised, Customer B's data is unaffected. The databases are encrypted with different keys. There is no shared secret. There is no single key that unlocks everything.
Here's what provisioning looks like at customer onboarding:
import { connect } from "@tursodatabase/serverless";
async function provisionCustomerDatabase(
customerId: string,
vault: SecretManager
) {
// Generate a unique key for this customer
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
const customerKey = Buffer.from(keyBytes).toString("base64");
// Store the key in your secret manager, not in any database
await vault.setSecret(`encryption-key-${customerId}`, customerKey);
// Create the customer's encrypted database via the Turso Platform API or CLI.
// tursoAdmin.createDatabase() is pseudocode; see the Turso Platform API docs
// for the actual call.
await tursoAdmin.createDatabase({
name: `customer-${customerId}`,
encryptionKey: customerKey,
encryptionCipher: "aegis256",
});
return connect({
url: `https://customer-${customerId}-myorg.turso.io`,
authToken: process.env.TURSO_AUTH_TOKEN,
remoteEncryptionKey: customerKey,
});
}
For security auditors, the story is straightforward: each tenant's data is encrypted at rest with a key that only that tenant (or your application acting on their behalf) controls. Turso never sees the plaintext. The key never touches Turso's storage. And because each tenant is a separate database rather than rows in a shared table, there's no risk of a query bug or permission misconfiguration leaking data across tenant boundaries.
This pattern works well for SaaS applications handling sensitive data, AI agent platforms that manage per-user data, and any architecture where a breach of one customer's credentials should not cascade to other customers. Poke does exactly this, giving every user their own database.
Picking a cipher shouldn't require a research project. Here's the decision in a table:
| Cipher | Use When | Key Size | Availability | Standards Status |
|---|---|---|---|---|
aegis256 | Most workloads; best performance on modern hardware | 256-bit | Local + Cloud | IRTF informational RFC, in review |
aegis128l | Want faster throughput, 128-bit key is acceptable | 128-bit | Local + Cloud | Same as above |
aes256gcm | Compliance requires NIST-approved cipher (HIPAA, PCI-DSS, SOC2) | 256-bit | Local + Cloud | NIST-approved. Auditors know it. |
aes128gcm | Compliance context, 128-bit key is acceptable | 128-bit | Local + Cloud | NIST-approved |
chacha20poly1305 | No hardware AES support (older mobile, some IoT/embedded) | 256-bit | Cloud only | IETF standard (RFC 8439) |
The short version:
If your auditor needs to recognize the cipher name, use aes256gcm. It's NIST-approved, well-understood, and you won't spend time in a compliance review explaining what AEGIS is. The performance difference compared to AEGIS is real but not meaningful for most workloads.
If you have freedom to choose, use aegis256. It's faster on modern hardware with AES instructions, it uses 256-bit nonces (which virtually eliminates nonce collision risk), and it's built on the AES round function. AEGIS is an IRTF informational RFC currently in final review at the RFC Editor (draft-irtf-cfrg-aegis-aead-18). IANA has already assigned algorithm IDs. But it is a proposed standard, not finalized, and it is not a NIST-approved standard. That distinction matters if you're checking compliance boxes.
If you're targeting hardware without AES instructions, use chacha20poly1305. It's software-optimized and performs well on older ARM chips and embedded systems. Note that it's only available in Turso Cloud, not in the local Turso Database engine.
Native rekeying (changing the encryption key of an existing database in place) is not yet supported in either Turso Database or Turso Cloud. Both list it as a planned feature in the documentation.
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-rekeyed \
--remote-encryption-key "${NEW_KEY}" \
--remote-encryption-cipher aegis256
# Import
turso db shell my-db-rekeyed --remote-encryption-key "${NEW_KEY}" < export.sql
This works but requires downtime and careful coordination. Encrypting existing unencrypted databases in place is also planned but not yet available. For now, adding encryption to an existing database means the same export/import cycle.
If you landed on this article directly and want the broader context on SQLite encryption options, how AEAD page-level encryption works at the architecture level, or how Turso compares to SQLCipher and SEE, Part 1 covers all of that. It's useful background if you're evaluating whether Turso is the right fit before committing.
Resources