When a team says "the data is encrypted," that statement is incomplete without answering three harder questions: where can plaintext exist, who holds the decryption keys, and what metadata remains visible even when message content is encrypted? TLS protects traffic in transit. Disk or filesystem encryption protects a stolen drive that is not mounted. Database-layer encryption or pgcrypto decrypts on the server — meaning a server-side attacker still has access to plaintext during that operation. Client-side or end-to-end encryption shifts the trust boundary to the client, but metadata about who communicates with whom, when, and at what frequency remains visible to the server. This article maps each layer precisely so you can make honest security claims about your system.
I recently went deep on this while designing the encryption architecture for a sensitive messaging application. The process forced me to write down exactly where each protection ends, what it cannot protect against, and what the server still sees regardless of which encryption layers are in place. That exercise is what this article is based on.
"Encrypted" Is Not a Complete Security Claim
The word "encrypted" has a precision problem. When a developer says their app encrypts data, it usually means one of several very different things: the connection uses TLS, the database lives on an encrypted disk, specific columns are encrypted at the application layer, or the client encrypts before the data ever touches the server. These four things look similar from the outside and are radically different from a threat modeling perspective.
The reason this matters is that each encryption layer protects against a specific threat. None of them protect against everything. A system that uses TLS is protected from network interception. That same system is completely unprotected if the server is compromised, because TLS terminates at the server and the server handles plaintext. Developers who conflate "we use TLS" with "the data is secure" are making a scope error — and it shows up in product copy, compliance documentation, and architecture decisions.
The useful question is: given my encryption stack, what can each party in the system actually see?
The Four Encryption Layers
Before going deeper, it helps to have a clear table of what each layer protects and where it stops.
This table should be on the wall of every team building an app that handles sensitive data. The pattern is clear: each layer moves the protection boundary one step closer to the client. The narrower that boundary, the smaller the set of parties who can ever access plaintext.
Where the Trust Boundary Actually Sits
The most important concept in encryption architecture is the trust boundary: the line that separates parties who can access plaintext from parties who cannot.
With TLS only, the trust boundary is at the network layer. Anyone with access to the server can read plaintext. This includes the application operator, the hosting provider, anyone with database access, and anyone who compromises the server.
With disk or filesystem encryption (like LUKS on a Linux VPS, or encrypted RDS storage), the boundary moves slightly. A physical disk theft or cold imaging attack yields encrypted data. The protection ends the moment the filesystem is mounted and the OS is running. From that point, the server reads and writes plaintext files. The PostgreSQL documentation states this directly: filesystem-level encryption protects against theft of a disk, but it provides no protection once the storage is mounted and the database is running. A live server compromise is fully outside its scope.
With application-layer encryption or pgcrypto, keys are stored server-side and decryption happens on the server. This adds meaningful protection against database-only exfiltration — an attacker who steals the database dump without the application server gets encrypted blobs. The problem is that the key is usually on the same server or accessible to the same process. An operator or a server-side attacker still has access to both the key and the data.
PostgreSQL's documentation on pgcrypto explicitly notes that decrypted data and decryption keys briefly exist in database server memory during pgcrypto operations. The protection is against offline database exfiltration only — not against a live server compromise.
With client-side or end-to-end encryption, plaintext never reaches the server. The server receives ciphertext, routes it, and stores it without ever seeing the plaintext content. This is the only architecture where the operator genuinely cannot read message content. The trust boundary is now at the client device.
What Metadata Still Leaks
This is where many end-to-end encrypted systems oversell their guarantees. Even when message content is perfectly opaque to the server, a significant metadata graph remains visible to the operator.
In a messaging or collaboration application, the server typically knows:
Who communicates with whom (communication graph)
When messages are sent and received
Message delivery state per recipient
Group membership changes and timing
Attachment sizes (statistical inference about media type is possible from size alone)
Login patterns, session durations, IP addresses, and device identifiers
Admin operations and user management events
The frequency and timing of key rotation events
This metadata graph is not zero-knowledge. In many threat models, metadata exposure is more damaging than content exposure. Communication patterns, contact graphs, and timing analysis can reveal sensitive facts about individuals even when the content of every message is encrypted.
Signal's protocol is end-to-end encrypted, and Signal's server infrastructure is designed to minimize metadata retention. Even so, Signal acknowledges that a subpoena can yield account creation date, last connection date, and phone number. Metadata is genuinely hard to eliminate at the server layer.
The honest security claim for an E2E encrypted application is not "we cannot see your data." It is "we cannot see the content of your messages, but we can see patterns about when and with whom you communicate."
Why Postgres Encryption at Rest Is Useful but Limited
PostgreSQL offers several encryption options, and it is worth being precise about what each one does.
Filesystem encryption (disk-level, via LUKS or cloud provider mechanisms like encrypted EBS) protects the raw bytes on disk. It says nothing about data in a running database. If you are on AWS RDS with encryption enabled, the protection is against AWS losing a physical disk or someone stealing a drive from a datacenter. It does not protect against a compromised application with database access.
pgcrypto allows encrypting specific values inside PostgreSQL using symmetric or asymmetric keys. pgp_sym_encrypt and pgp_sym_decrypt run inside the database process. The key has to be supplied by the calling application. If the application and the database are on the same server — which is common — the protection is limited. A server-level attacker has access to both.
Application-layer encryption before the database write is stronger in one specific way: the key never passes through the database. The application encrypts the value in memory, writes the ciphertext to PostgreSQL, and decrypts it after reading. The database stores blobs it cannot interpret. This protects against database-only exfiltration even if the key is not stored in the DB.
code
Application process
plaintext ──encrypt(key)──> ciphertext
PostgreSQL
stores ciphertext only
key never present in DB process
The remaining exposure is that the key and the decrypt path still live in the application server. A server-side compromise still yields plaintext. The useful threat it defeats is a scenario where an attacker can query the database but cannot execute application code — for example, a SQL injection attack that can exfiltrate rows.
If your goal is to protect against database exfiltration without protecting against server compromise, application-layer AES encryption before the write is the right pattern. Use a 256-bit key loaded from a secret manager (not from an environment variable, which can leak in process metadata and container inspect output), generate a cryptographically random nonce per record, and bind the ciphertext to authenticated additional data (AAD) so records cannot be transplanted between tables.
The bottom line on Postgres encryption at rest: it provides a real and useful layer of defense in depth. It is not a substitute for defining who can access plaintext at the application layer.
Why E2E Apps Still Need Server Hardening
A common assumption is that once end-to-end encryption is in place, server security matters less because the server cannot read message content anyway. This assumption is wrong.
A server that cannot read content can still:
Disrupt or manipulate delivery. It can block messages, delay them selectively, or replay old messages. It cannot forge content, but it can interfere with delivery integrity.
Observe metadata at scale. As covered above, the communication graph, timing, group membership, and delivery state are all visible to the server operator.
Attack the key exchange. End-to-end encryption is only as strong as the key exchange phase. If the server can substitute a public key during contact exchange — serving a different HPKE or ECDH public key than the one the user registered — an active man-in-the-middle attack is possible. This is why key fingerprint verification out-of-band matters.
Leak backups, WAL, and logs. An E2E encrypted application that runs PostgreSQL with WAL archiving enabled is writing its encrypted message blobs to a WAL stream that replicates to a backup server. If those backups are not secured equivalently to the primary database — same encryption, same access controls — the kill switch and the security claim are both weakened.
Expose encrypted blobs plus metadata together. An attacker who obtains both the encrypted blobs and the full metadata graph has a richer dataset than either alone. The metadata provides context for timing attacks and correlation. The blobs become the target of future cryptanalysis or key compromise.
Hardening an E2E server means treating the metadata tables, the backup pipeline, the WAL archive, and the application logs as sensitive data even though they contain no plaintext message content.
The Endpoint Problem: Plaintext Exists on the Device
Any honest encryption architecture guide has to end at the device, because that is where plaintext always exists. The client decrypts the message and renders it. At that point, the protection model shifts entirely.
On Android, FLAG_SECURE prevents message content from appearing in screenshots and in the recent apps thumbnail. It does not prevent accessibility services with screen reader permissions from observing displayed content. It does not prevent a rooted device from reading process memory. It does not prevent physical observation of the screen.
FLAG_SECURE is a UI-level protection. It tells the Android window manager to treat the surface as secure and exclude it from system captures. It does not protect against accessibility services, elevated-permission processes, OEM memory snapshot configurations, or physical observation.
The realistic device threat model looks like this:
code
Device protections:
FLAG_SECURE ─────────────> prevents screenshots, thumbnails
Android Keystore ─────────> protects signing/encryption keys
SQLCipher (local store) ──> protects local state at rest
Device exposure:
Accessibility services ───> can observe displayed content
Rooted device ────────────> can read process memory
Active session ───────────> plaintext in memory during use
Physical observation ─────> no technical countermeasure
The correct framing is that E2E encryption protects message content in transit and at rest on the server. It does not protect against a compromised device, an active session on a seized device, or physical screen observation. These are legitimate limitations that should be communicated honestly.
Keyboard autocorrect and IME (input method editor) access to message input fields is a commonly overlooked exposure. If the system keyboard has IME learning enabled, typed message content may be written to the keyboard's prediction database. Message input fields should disable autocorrect and block IME memory.
A Practical Checklist Before You Make a Security Claim
Before publishing a privacy or security page for an application that handles sensitive data, work through these questions. Each one maps to a real exposure that has appeared in real products.
Plaintext and key scope
Where does plaintext exist in your system? List every location explicitly.
Who holds decryption keys? Is it the operator, the user, or both?
Can the operator decrypt content without the user's involvement?
Database and storage
Are database backups encrypted with the same key, a different key, or not at all?
Is WAL archiving or point-in-time recovery enabled? Are those archives secured equivalently?
Are filesystem snapshots included in the threat model?
Logs and metadata
Do application logs contain any content fragments, key material, or identifiers that link to users?
Does the metadata graph (who talks to whom, when, delivery state) reveal facts you have not disclosed?
Are container logs redirected to an encrypted store with short retention?
Key management
Is the storage key loaded from a secret file or from an environment variable? (Environment variables appear in process metadata and container inspect output.)
What happens when a user's device is lost? Is there a recovery mechanism? Does that mechanism weaken the security claim?
What happens if an admin key is lost?
Endpoint
Are message input fields blocking IME memory and autocorrect?
Does decrypted content get written to any persistent storage, cache, or log?
What is the security claim for an active session on a seized device?
The honest claim
What security claim does the product make, and is every word of it accurate given the above?
What can the operator see, and is that disclosed to users?
FAQ
Is pgcrypto enough to protect database contents from a compromised server?
No. pgcrypto decrypts inside the PostgreSQL process, and the key is supplied by the application. If the application server is compromised, the attacker has access to both the key and the decrypt path. pgcrypto is useful for protecting against database-only exfiltration — an attacker who can query the database but cannot run application code — which is a real and valuable threat to address. It is not a protection against server-level compromise.
Does end-to-end encryption mean the server sees nothing?
No. The server cannot read message content, but it can still observe the metadata graph: who communicates with whom, when, how often, group membership changes, delivery state, attachment sizes, and login patterns. In many threat models, this metadata is the sensitive dataset.
What is the risk of storing the encryption key in an environment variable?
Environment variables are visible in process metadata, which means they can appear in crash dumps, container inspect output, process listings on shared hosts, and shell history. A Docker secret file loaded at startup and never logged is meaningfully safer. The key material should also be zeroed in memory during shutdown sequences where possible.
If WAL archiving is enabled, does the kill switch still work?
A kill switch that zeros the in-memory storage key and truncates the live database tables has no effect on WAL archives, filesystem snapshots, or off-site backups. Those copies retain the encrypted blobs. Whether that is a problem depends on whether the attacker also has the storage key — but in a seizure scenario, that cannot be assumed. High-sensitivity deployments should explicitly decide whether to disable WAL archiving and automated snapshots, understanding the tradeoff between operational resilience and kill-switch effectiveness.
How do I know if my security claim is honest?
Write down every party who can access plaintext, under what conditions, and what metadata each party can observe. Then read your security page and check whether the copy reflects what you actually wrote down. Most claims fail this check.
Conclusion
Every encryption layer has a specific threat it addresses and a clear boundary where its protection ends. TLS stops at the server. Disk encryption stops when the filesystem is mounted. Application-layer database encryption stops at the server-side decrypt path. End-to-end encryption moves the plaintext boundary to the client device — and the device itself has its own exposure surface.
The metadata graph is the piece most security claims skip. Even a perfectly implemented E2E encrypted system leaves the communication graph, timing, group membership, and delivery state visible to the server operator. That is not a failure of encryption. It is an honest limitation that should be stated clearly.
Building a system that handles sensitive data well means being precise about each of these boundaries, designing the key management to match the actual threat model, and writing security claims that reflect what the system actually protects. Anything else is marketing.
Let me know in the comments if you have questions, and subscribe for more practical development guides.
Thanks,
Matija
Layer
Protects against
Does not protect against
TLS
Network interception, passive eavesdropping
Server/operator access, compromised server
Disk / filesystem encryption
Stolen physical disk, offline cold-boot
Mounted filesystem, live server compromise
Database / column encryption (pgcrypto, AES at app layer)