Skip to main contentSkip to main content
Back to blogBehind the Scenes

Encryption at Rest: How We Protect Every Conversation You Have With Ophie

Ophie Team May 7, 2026 9 min read

When you tell Ophie about a fight you had with your partner, a grief you can't shake, or a thought you've never said out loud — that's some of the most private content that will ever exist about you. The engineering question we've been sitting with for the past few months is simple to ask and hard to answer well: how do we make sure those conversations stay yours, even from us?

This post is the long version. The short version lives at /security, and the audit-grade technical details are in our Trust Center. If you're curious why this took the shape it did and what we chose to be honest about, keep reading.

Why this matters more for a mental health app

Most apps store user data and call it a day. The threat model for a productivity tool is: don't leak the spreadsheet. The threat model for a mental health app is different. The data is sensitive in a way that doesn't map cleanly onto regulatory categories — it's often not technically PHI, it's usually not legally a medical record — but if a transcript ever ended up somewhere it shouldn't, the harm wouldn't be measured in dollars. It would be measured in someone deciding never to talk to anyone again.

We don't want users to trust us not to read their sessions. We want it to be true that we can't, outside a narrow auditable window, and impossible after deletion.

The threat model we actually care about

Before we picked any cryptography, we wrote down the threats we wanted to defend against — and the ones we didn't pretend we could.

What we defend against: a leaked database backup, a compromised database vendor, a stolen service-role credential, an over-broad debugging session, and the most important one — future us. The Ophie of three years from now, after Series A, after a team grew, after some engineer had a question about why a session ended weirdly. Future us shouldn't have a button labeled "read user transcript." The whole point of this work is to make sure we never accidentally build that button.

What we don't pretend to defend against:a fully compromised server in the middle of an active session. For Ophie to do real-time voice — speech-to-text, an LLM that reasons about what you just said, text-to-speech — the server has to hold your encryption key in memory, briefly, while you're talking. Some products in adjacent spaces claim end-to-end encryption. We don't. End-to-end encryption with a server-side LLM is essentially a contradiction, and we'd rather be honest than market a guarantee we can't keep.

Envelope encryption, in plain English

The pattern we landed on is called envelope encryption. It works like this: every user gets their own encryption key — we call it a Data Encryption Key, or DEK. That key is itself locked inside a master key, which lives in AWS KMS — a hardware security module that we cannot extract from. The wrapped (locked) version of your key is stored on your account. The unwrapped version exists only in memory, only during your sessions.

When you start a session, Ophie's agent asks AWS to unwrap your key. AWS logs the request. The agent uses the unwrapped key to encrypt your transcript with AES-256-GCM — a standard algorithm with authenticated encryption that lets us tie each ciphertext to its rightful owner. When the session ends, the unwrapped key drops out of memory.

Why this shape? A few reasons:

  • Per-user keys.A breach affecting one user's key cannot decrypt another user's data. There is no master per-app key whose theft would unlock everyone.
  • Hardware-backed master key.AWS KMS keys never leave the HSM. Even AWS staff cannot export them. We can't download them. Nobody can.
  • Audited decryption. Every unwrap is a CloudTrail event. If an engineer ever decrypts a row outside an active session, it leaves a paper trail with their identity, the user ID, and the timestamp.
  • Crypto-shredding on deletion.Destroying one small key destroys the readability of every row encrypted with it. We don't have to wait for backups to roll over.

What actually gets encrypted

Everything that's content: full session transcripts, summaries, derived insights, long-term memories, mentions of people you talked about, emotional snapshots, journal entries from activities. All of it stored as ciphertext, all of it bound to your user ID with authenticated metadata so that an attacker can't copy one user's ciphertext into another user's row.

What stays in the clear: operational data. Session start and end times, durations, error codes, your account email, authentication tokens. We need this stuff to run the service and we'd rather be transparent about its existence than pretend everything is encrypted when it isn't. None of it contains conversational content.

One thing envelope encryption makes hard: searching encrypted data. You can't do a SQL WHERE name = 'Mom' on ciphertext. For most things this is fine — Ophie doesn't offer text search of your transcripts at the database level. But for topic-people links(rows that say "this session mentioned this person") we needed equality lookup without decrypting.

The solution is an HMAC-blinded fingerprint. We compute a keyed hash of the person's name using a per-user HMAC key (also wrapped under your DEK) and store that fingerprint in an indexed column. Two rows with the same name produce the same fingerprint for the same user— but a different user asking about "Mom" gets a different fingerprint, so cross-user comparison is impossible. The actual name is still ciphertext.

Deletion is a one-way operation

This is the part we're proudest of. When you delete your Ophie account, the very first thing that happens is your DEK is set to NULL in the live database. Your in-process key cache is evicted. Your encrypted rows are tombstoned. Your Pinecone vectors are deleted via API.

After the Postgres point-in-time recovery window closes — the backup horizon — a background sweep permanently hard-deletes the tombstoned rows. At that point, even a backup restore cannot bring your content back, because the key needed to decrypt it no longer exists in any backup.

Most products quietly hold onto deleted data "for backups" for 30 to 90 days. We do too — but the ciphertext in those backups is unreadable the moment your key is destroyed.

What we're honest about

Marketing pages love absolutes. We tried to write ours without any. There are real caveats and we'd rather you find them here than in a blog post about us:

  • This is not end-to-end encryption. The server holds your key in memory during your sessions. We explain why above.
  • Voice subprocessors handle audio in transit. LiveKit transports the audio, Deepgram transcribes it, Cartesia synthesizes the response. Each operates under its own retention policy, listed in our subprocessor index.
  • Pinecone sees embeddings. Vector representations of your memories are needed for similarity search. We pair them with an opaque ciphertext sidecar — the actual content requires KMS decryption to read — but the embedding itself is not encrypted at the vendor side.
  • The cache eviction window is not instantaneous. When you delete your account, your key is NULLed in the database immediately, but another worker process that already loaded it may keep it in memory for up to 10 minutes (the cache TTL).
  • This has not been externally audited. Yet. Every encryption-touching change goes through a self-review log committed to the repo, and the architecture is documented openly. External audit is on the roadmap.

What it took to build this

Eleven distinct phases. Sixty-five engineering plans. A terraform module for AWS KMS. A migration that touched every table holding user content. A dual-write soak window where ciphertext and plaintext lived side by side until we were certain decryption worked, before we dropped the plaintext columns. A nonce-counter hard-stop that prevents nonce reuse even under pathological write loads. A custom self-review PR template so encryption changes can never be merged silently.

The unsexy answer to "why did this take months" is that the failure modes of encryption work are subtle and mostly invisible. A bug here doesn't crash the app. It quietly produces ciphertext that can never be decrypted, or decrypted ciphertext that gets logged somewhere it shouldn't. Every step had to be reversible until the last step, which by design isn't.

What's next

The architecture is live. Every new session you start with Ophie is encrypted under this model. Existing sessions were backfilled. The decision now is what to harden next: external audit, automatic per-user key rotation, optional CMEK for Pinecone if they add it to the Standard tier. We'll update the changelog when those land.

If you're a security researcher and you spot something we've gotten wrong, please email team@ophie.app. We'll update the docs and credit you.

If you're a user wondering whether your stuff is safe: it is, in the specific ways described above, and we'll keep being explicit about the parts that aren't. The whole point of working at this for as long as we did is that we wanted you to be able to talk to Ophie about anything — without having to think about who else might one day read it.