Pillar 3 ยท Implementation

How to build zero-knowledge
on Vercel

Zero-knowledge means: the server stores your data, but cannot read it. Not because encryption is good, but because the server never sees plaintext. Here's the architecture and code to make it real on Vercel.

Backend Architecture April 15, 2026 15 min read

Most "zero-knowledge" apps claim it in marketing. Few actually implement it.

True zero-knowledge means: the server cannot decrypt your data, even if it wanted to. Even if law enforcement subpoenas it. Even if the server gets hacked. The server sees only encrypted bytes.

This isn't theoretical. CHRONOS implements it. Here's how.

Zero-knowledge isn't a feature. It's an architecture. Build it from the beginning or don't build it at all.

The core principle: Encryption before transmission

Zero-knowledge has one rule: plaintext never leaves the client.

Here's what happens:

Zero-knowledge data flow

1
Client: User writes
User enters "My secret thought" in the CHRONOS app. This lives in plaintext in the browser.
2
Client: Encrypt before leaving
Before any network call, AES-256-GCM encrypts the thought. Only the encrypted bytes leave the browser.
3
Network: Transmission
Encrypted blob travels over HTTPS to Vercel. Even if HTTPS is intercepted, only ciphertext is visible.
4
4
Server: Storage
Vercel stores the encrypted blob in a database. Server has zero visibility into content. Subpoenas get ciphertext, not secrets.
5
Client: Decrypt on retrieval
When user retrieves, encrypted blob returns to browser. Client-side decryption happens only on the user's device. User reads plaintext.

The critical insight: Vercel never holds the decryption key. The key stays on the user's device. The server cannot decrypt even if it tries.

The technical implementation: How CHRONOS does it

Here's the actual code pattern:

JavaScript / Node.js
// 1. Client-side encryption (happens BEFORE sending) import { webcrypto } from 'crypto'; async function encryptVault(plaintext, userKey) { // userKey = 256-bit key (only user has this) // AES-256-GCM: authenticated encryption (prevents tampering) const iv = webcrypto.getRandomValues(new Uint8Array(12)); const encrypted = await webcrypto.subtle.encrypt( { name: 'AES-GCM', iv }, userKey, new TextEncoder().encode(plaintext) ); // Return encrypted blob + IV (IV is public, doesn't compromise security) return { iv: Array.from(iv), ciphertext: Array.from(new Uint8Array(encrypted)) }; } // 2. Send encrypted blob to Vercel (plaintext never transmitted) const encrypted = await encryptVault(userThought, userKey); const response = await fetch('/api/vault', { method: 'POST', body: JSON.stringify(encrypted) // Only ciphertext }); // 3. Server stores encrypted blob (never sees plaintext) // Vercel API handler export default async function handler(req, res) { const { iv, ciphertext } = req.body; // Store as-is. Server cannot decrypt. await db.vaults.create({ userId: req.user.id, iv, ciphertext, createdAt: new Date() }); return res.json({ success: true }); } // 4. Client-side decryption (only on user's device) async function decryptVault(encrypted, userKey) { const decrypted = await webcrypto.subtle.decrypt( { name: 'AES-GCM', iv: new Uint8Array(encrypted.iv) }, userKey, new Uint8Array(encrypted.ciphertext) ); return new TextDecoder().decode(decrypted); } // User retrieves vault const vault = await db.vaults.findById(vaultId); const plaintext = await decryptVault(vault, userKey); // Only now is plaintext visible (in browser)

That's it. The pattern is:

  1. User enters plaintext
  2. Encrypt client-side with user's key
  3. Send only ciphertext to server
  4. Server stores ciphertext
  5. When retrieving, client decrypts

The server never touches plaintext. Ever.

If the server never sees plaintext, it cannot leak it, sell it, or be compelled to hand it over.

The key problem: Where does the encryption key live?

This is the hardest part of zero-knowledge architecture.

You need a cryptographic key to encrypt/decrypt. Where should it live?

๐Ÿ”‘
Option 1: User password
Derive encryption key from user password (PBKDF2, Argon2). User remembers password, key is derived on-the-fly. Trade-off: weak password = weak encryption.
๐Ÿ’พ
Option 2: Browser storage
Store random key in browser localStorage/IndexedDB. Fast, strong. Trade-off: if device is compromised, key is accessible.
โšก
Option 3: Hardware security
Store key in secure enclave (iOS, Android, Windows Hello). Most secure. Trade-off: only works on supported devices.
๐ŸŽฒ
Option 4: Hybrid
Combine password + stored key. User password unlocks storage key. Best of both worlds, added complexity.

CHRONOS uses Option 1 (password-derived key) for simplicity + Option 2 (browser storage) for convenience. The tradeoff: passwords are the weakest link.

This is why CHRONOS emphasizes: use a strong, unique password. Your encryption is only as strong as your password.

Building zero-knowledge on Vercel: The architecture

Here's how to structure the app:

1

Client (React / Frontend)

Where encryption happens. User writes plaintext. Client-side JS encrypts before sending. Client decrypts on retrieval. Plaintext never leaves this layer except within the user's browser memory.

2

API (Vercel Functions)

Stateless endpoints. Receive encrypted blob. Store blob. Retrieve blob. Return blob. Never touches plaintext. Can be written in Node, Python, Go โ€” doesn't matter. It doesn't see secrets.

3

Database (Postgres, MongoDB, etc.)

Stores encrypted blobs and metadata. Never stores keys. Never sees plaintext. Could be compromised; attackers get random bytes, not secrets.

4

Authentication Layer

Verifies user (JWT, sessions). But does NOT encrypt/decrypt. API layer just checks "is this user allowed to access this vault?" then returns encrypted blob to client.

The key principle: Encryption/decryption logic is client-only. The server is stateless about plaintext.

The honest trade-offs: What zero-knowledge costs

Zero-knowledge is powerful but expensive:

Trade-off #1: No Server-Side Search

With encrypted data, the server cannot search your vaults. It can't find "all entries from January" or "all entries containing 'anxiety'." You can only search on the client. This limits UX (search is slower, only works on downloaded data).

Trade-off #2: No Server-Side Sync Without Trust

When you sync across devices, encrypted data must match. But "match" is hard to verify when you can't decrypt. CHRONOS solves this with device isolation + timestamps, but it's complex.

Trade-off #3: Metadata Still Exists

Server sees: when you write, how often, file sizes, device info, IP address. This metadata alone reveals patterns (behavior, location, emotional state changes). Zero-knowledge encrypts content but not metadata.

Trade-off #4: Complexity for Developers

Encryption/decryption logic is tricky. Mistakes are catastrophic. Key management is hard. Most teams don't build this correctly. Zero-knowledge requires serious engineering discipline.

Despite these costs, the benefit is profound: your data is yours, not the platform's.

The cryptography: What makes it actually work

Zero-knowledge relies on three cryptographic primitives:

Together: AES-256-GCM with PBKDF2 key derivation is the industry standard for zero-knowledge apps. It's what Signal, Telegram (optional), and CHRONOS use.

The security is proven. The weakness is always the human side: password strength, key management, device security.

Implementation checklist: Building zero-knowledge correctly

If you're building zero-knowledge:

โœ“

Use a proven library

Don't implement encryption yourself. Use libsodium (Sodium.js), TweetNaCl.js, or Web Crypto API. Let cryptographers handle the hard parts.

โœ“

Never log plaintext

Not in server logs, not in error messages, not in monitoring. If plaintext appears anywhere server-side, you've broken zero-knowledge.

โœ“

Encrypt before sending

Always. No exceptions. If you ever transmit plaintext, zero-knowledge is broken. Make encryption the default step before any HTTP call.

โœ“

Use strong key derivation

PBKDF2 with 600,000 iterations, or Argon2. Weak key derivation is the #1 way zero-knowledge fails.

โœ“

Audit for metadata leaks

Even with encryption, metadata can reveal patterns. Think about what the server learns: timestamps, file sizes, sync patterns, device info. Minimize what's logged.

โœ“

Test the failure case

What happens if the encryption key is forgotten? Make sure data isn't unrecoverable. Plan for key recovery (backup codes, device syncing).

This checklist is not exhaustive. Building privacy-respecting systems is hard. Hire security experts if you're handling sensitive data.

Why Vercel? Why serverless?

Zero-knowledge can be built on any backend, but serverless (Vercel Functions) has advantages:

That said, zero-knowledge can run on traditional servers (AWS EC2, DigitalOcean, your own hardware). The architecture is backend-agnostic.

Zero-knowledge isn't about the platform. It's about the principle: plaintext never leaves the client.

The shift in software design

Zero-knowledge represents a fundamental shift in how we think about user data:

The new model doesn't require trust. It requires architecture.

This is why CHRONOS was built zero-knowledge from day one. Not because we're unusually ethical, but because the architecture is becoming table-stakes for apps handling sensitive data (journals, health records, financial data).

If you're building an app that stores user secrets, build it zero-knowledge. If you can't, acknowledge it. Don't claim privacy you don't have.

CHRONOS

Built zero-knowledge
from day one.

Your vault is encrypted on your device. We literally cannot read it. That's not a promise. It's an architecture.

Open CHRONOS