Skip to main content
Ragen offers opt-in AES-256-GCM envelope encryption for conversation data at rest. Each conversation thread gets its own data encryption key, which is itself encrypted by a master key held in your key provider. The master key never leaves your control.
Encryption is off by default. With no key provider configured, Ragen starts normally and stores message content in plaintext. Local development works this way intentionally, but a production deployment must configure a provider explicitly. Verify that new threads are being encrypted by checking the SQL query in the Verifying encryption is active section below.

Choosing a key provider

Set ENCRYPTION_PROVIDER to one of the following values and supply the required variables: If you leave ENCRYPTION_PROVIDER unset, Ragen auto-detects a provider by checking in order: Scaleway → AWS KMS → local. With none configured, encryption remains off.
The local provider is for development only. Do not use ENCRYPTION_MASTER_KEY as your sole encryption backend in production — it stores the master key on the same machine as the data it protects.

Ragen checks the key at startup

Setting the variables is not the same as being able to use the key. Credentials without permission on that specific key, a key id belonging to a different project, or a region that does not hold the key all look like a correct configuration and fail on the first message. So at startup Ragen generates one throwaway data key and unwraps it again, then discards both. Two calls, not one: encrypting and decrypting are separate permissions on both Scaleway Key Manager and an AWS key policy, and a principal that may wrap but not unwrap writes messages nobody can ever read back. “Refused” means a permanent answer: any 4xx other than 408 or 429, an AWS refusal, a malformed response, or a data key that comes back as different bytes than were wrapped.
ALLOW_UNENCRYPTED=1 does not override a refused key. That variable waives the requirement to configure encryption; it does not make a broken key work. Because a key provider is still configured, message writes keep calling it and keep failing — so Ragen reports the refusal rather than promising a plaintext fallback that does not exist. Remove the provider’s variables if you genuinely intend to run without encryption.
When a deployment is refused, check the three things that produce an identical permission error: whether these credentials may use this key id in this region. Any of the three can be wrong on its own, and the error text is the same in each case.

How envelope encryption works

1

Thread key generation

When a new conversation thread is created, Ragen requests a fresh data encryption key (DEK) from your configured key provider. The provider returns both the plaintext DEK and a wrapped (encrypted) copy of that DEK.
2

Message encryption

All message content written to that thread is encrypted using the plaintext DEK with AES-256-GCM. The plaintext DEK is held only in memory for the duration of the request and never written to disk.
3

Wrapped key storage

The wrapped DEK is stored alongside the thread record in Postgres. To decrypt a thread later, Ragen unwraps the DEK using the key provider, then uses it to decrypt the message content.
4

Thread titles stay plaintext

Thread titles are stored in plaintext deliberately so that title search continues to work. Content search over encrypted threads is title-only — a deliberate trade-off.

In-transit encryption

TLS secures all connections to the application, terminated by your own ingress. Internal service-to-service calls are additionally authenticated with HMAC-signed short-lived tokens, so no internal component can be impersonated from within the network.

Impact on features

Enabling encryption changes the behaviour of two features:

Langfuse traces

With encryption on, input and output fields are omitted from Langfuse traces. Only tags, session ID, and model are recorded — message content is never sent to the tracing backend.

Content search

Search across threads matches on thread titles only. Message content is not searched whether or not it is encrypted.

Consciously opting out

If you have evaluated the trade-offs and need to run a deployed environment without encryption — for example, during a migration — set ALLOW_UNENCRYPTED=1 and configure no key provider. This is a conscious, logged opt-out:
Setting ALLOW_UNENCRYPTED=1 is logged as a critical severity ENCRYPTION_REQUIREMENT_BYPASSED security event each time the application starts. This event appears in your security event log and, if SECURITY_ALERT_SEVERITY is set to critical, triggers an email alert.

Verifying encryption is active

After configuring a key provider and restarting your deployment, confirm encryption is working by checking a newly created thread in Postgres:
Rows where encrypted_dek is populated are encrypted. If encrypted_dek is null on new threads, the key provider configuration has not reached the process — recheck your environment variables and restart.
A key that is configured but refused does not reach this check: the startup probe stops the deployment first. Reaching a running application with null encrypted_dek on new threads therefore points at variables that never arrived, not at a key the provider rejected.

Encrypting existing threads

If you enable encryption on a deployment with existing plaintext threads, use the Encrypt All Threads action in the admin panel (Organizations → [org] → Settings). This action is available to platform administrators only. It is idempotent and resumable — safe to run multiple times if interrupted.