Encryption
Field-level envelope encryption with per-organization keys. Mark a column .encrypted() and a database breach yields only ciphertext.
Mark a text column .encrypted() and Quickback envelope-encrypts it at rest with a per-organization key:
export default feature('guests', {
columns: {
id: q.id(),
organizationId: q.scope('organization'), // required — keys are per-org
name: q.text().required(),
emergencyContact: q.text().encrypted(),
dietaryNotes: q.text().encrypted(),
},
read: { access: { roles: ['member+'] } },
create: { access: { roles: ['member+'] } },
});That one modifier changes what a database breach means: a leaked D1 backup, export, or stolen database token yields only ciphertext for those columns. The keys live separately, wrapped under a worker secret the database never sees.
What the compiler generates
org_data_keys— one row per organization holding a random 256-bit data key (DEK), stored wrapped under theENCRYPTION_KEKworker secret. Created lazily on the org's first encrypted write.- Write path — every write (CRUD routes, batch routes, and
db.insert/db.updateinside your actions) flows through the same chokepoint, which AES-256-GCM-encrypts the column before anything else sees it. Audit snapshots, webhook payloads, and realtime deltas therefore carry ciphertext by construction. - Read path — generated routes decrypt behind the firewall and then apply masking, so authorized callers see plaintext and everyone else sees the redaction — never ciphertext.
ctx.crypto— explicitawait ctx.crypto.decrypt(value)/ctx.crypto.encrypt(value)for action code that processes the field server-side (building a manifest, sending an email). Scoped to the caller's active organization.- Boot guard — a project with encrypted columns refuses to serve without
ENCRYPTION_KEK(503MISSING_ENV), instead of silently storing plaintext.
Values at rest look like qbenc:1:<keyVersion>:<iv>:<ciphertext> — the prefix makes accidental plaintext detectable, and the embedded key version supports rotation with lazy re-encryption on write.
Setup
Scaffolded projects already have a generated ENCRYPTION_KEK in .dev.vars, so local dev works with zero setup. For production:
openssl rand -hex 32 | wrangler secret put ENCRYPTION_KEKRules the compiler enforces
Ciphertext cannot be queried, so encrypted columns are excluded from every query surface at compile time — these are errors, not warnings:
- No
.filterable()/.searchable(), and never listed in a view'squery.{searchable,filterable,sortable} - No
.unique()(AES-GCM randomizes ciphertext per write), no primary keys, no.references(), noq.scope()columns, no defaults (a SQL default would bypass the encrypting chokepoint) - Never an
embeddings.fieldssource (embedding a value ships its plaintext to the model) - The table must carry a
q.scope('organization')column — keys are per-organization - Text-shaped columns only (v1)
Encrypted columns may appear in view projections and responses — they decrypt through the generated prepare path. Masking composes on top as defense-in-depth.
Per-organization keys: blast radius and crypto-shredding
Each organization's data is encrypted under its own DEK, so one compromised key never exposes another tenant. Deleting an org's org_data_keys row makes every encrypted value it ever wrote permanently unreadable — the cleanest possible GDPR-erasure and offboarding story.
What this tier does and doesn't claim
The envelope tier protects against storage compromise: backups, exports, dashboard access, leaked database tokens, SQL injection, and insiders with database read access all get ciphertext. The server can decrypt — that's the point; your actions still process the field. It does not protect against full Worker compromise (the sealed/E2EE tier in the encryption roadmap addresses that — see apps/compiler/research/encryption-vault-spec.md).
Two practical notes:
- Action responses: rows returned from
db.insert(...).returning()inside your own actions contain ciphertext — decrypt explicitly withctx.crypto.decrypt()if your action returns the field. Generated CRUD routes handle this automatically. - No server-side equality lookups on encrypted values (blind indexes are a roadmap item). If you need to filter by it, it probably shouldn't be encrypted — or it needs a separate non-sensitive derived column.
Masking - Field Redaction
Hide sensitive data from unauthorized users with automatic field masking. Secure PII and confidential fields while maintaining access for authorized roles.
Views - Column Level Security
Named field projections with per-view access control. Views live under the read pipeline (read.views) and implement column-level security.