Alok Logo

Adding Encryption to Notes

To add encryption to your notes with a stack of Vercel Blob + Neon Postgres + Clerk Auth, you need to decide where the decryption key lives. This determines your security model: Authenticated Server-Side Encryption (easier to implement, allows server-side search) or End-to-End Encryption (E2EE) (maximum privacy, client handles keys).

Given your setup, implementing Authenticated Server-Side Encryption using a single master key stored in environment variables, combined with Clerk's user IDs, provides an excellent balance of security and performance.

Here is how you can implement encryption across both your database and blob storage.

1. Encrypting Text Notes in Neon DB (Field-Level Encryption)

Instead of encrypting the entire database, you should encrypt just the sensitive text field (e.g., the content column) before it leaves your Next.js server and hits Neon.

The Cryptographic Approach

Use Node’s native crypto module with the AES-256-GCM algorithm. It provides authenticated encryption, meaning it ensures the data hasn't been tampered with.

  • The Secret Key: Generate a cryptographically secure 32-byte key and store it in your Vercel environment variables (ENCRYPTION_KEY).

  • The Initialization Vector (IV): Crucial for security—never reuse an IV. Generate a fresh 12-byte random IV for every single note save. You store the IV alongside the encrypted text in your database.

Database Schema Adjustments

Modify your Neon Postgres table schema (whether using Prisma, Drizzle, or raw SQL) to store the encrypted payload components:

SQL

ALTER TABLE notes ADD COLUMN encrypted_content TEXT;
ALTER TABLE notes ADD COLUMN iv TEXT;
ALTER TABLE notes ADD COLUMN auth_tag TEXT;
ALTER TABLE notes DROP COLUMN content; -- Remove plain text

Server-Side Implementation (Next.js Server Actions / API Routes)

When a user saves a note, encrypt it on the server after verifying their Clerk session:

JavaScript

import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes

export function encryptText(plainText) {
  const iv = crypto.randomBytes(12); // Always random
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
  
  let encrypted = cipher.update(plainText, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  
  const authTag = cipher.getAuthTag().toString('hex');
  
  return {
    encryptedContent: encrypted,
    iv: iv.toString('hex'),
    authTag: authTag
  };
}

export function decryptText(encryptedContent, ivHex, authTagHex) {
  const iv = Buffer.from(ivHex, 'hex');
  const authTag = Buffer.from(authTagHex, 'hex');
  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
  
  decipher.setAuthTag(authTag);
  
  let decrypted = decipher.update(encryptedContent, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  
  return decrypted;
}

2. Encrypting File Attachments in Vercel Blob

If your notes app allows users to upload images, PDFs, or audio snippets, Vercel Blob stores these via public URLs. To secure them, you must encrypt the raw file buffers before uploading them to Vercel Blob.

The Upload Flow

  1. Client: The user drops a file into the editor.

  2. Server Route: Instead of using client-side uploads, send the file to a Next.js Server Action or API Route protected by auth() from Clerk.

  3. Encryption: On the server, encrypt the file buffer using the same AES-256-GCM strategy.

  4. Vercel Blob Storage: Upload the encrypted buffer to Vercel Blob. Save the resulting file URL, IV, and Auth Tag into your Neon DB linked to that note ID.

The Read/Download Flow

Because the file on Vercel Blob is now scrambled ciphertext, anyone trying to access the direct public URL will just download a corrupted, unreadable file.

  1. When your editor loads a note, your server fetches the Vercel Blob URL from the Neon database.

  2. The server downloads the encrypted buffer from Vercel Blob internally.

  3. The server decrypts the buffer using the corresponding IV/Auth Tag.

  4. It streams the decrypted file data back to the client or generates a temporary, secured object URL.

The Clerk Auth Security Boundary

Clerk plays a critical role here to ensure User A can never decrypt User B's notes.

  • Tenant Isolation: In every Neon database query, append a strict user check:

    SQL

    SELECT encrypted_content, iv, auth_tag FROM notes WHERE id = $1 AND user_id = $2;
    

    Where $2 is strictly populated by Clerk’s server-side auth().userId.

  • Even if an attacker somehow bypasses your database permissions, they cannot decrypt the content without access to your Vercel server environment’s master ENCRYPTION_KEY.

Alternative: End-to-End Encryption (E2EE)

If you want a zero-knowledge architecture where not even you (the developer) can read the notes:

  • You must encrypt the text directly in the user's browser using the native Web Crypto API before sending anything over the network.

  • The Catch: The decryption key must be derived from the user's master

  • password (using PBKDF2). Since the Clerk doesn't pass the raw password to your app architecture, your users would have to create a separate "Secret Passphrase" explicitly for their notes layer. If they lose that passphrase, their data is lost forever, and server-side text searching becomes impossible.

I don’t know what is happening here. Kuch bhi ho raha. This is so stupid. But it’s fun.

Ran command: git statusRan command: git add src/app/components/ActionPuck.tsx src/app/components/NoteEditor.tsx public/sounds/clink.wav public/sounds/tick.wav public/sounds/type.wav && git commit -m "Add haptic UI sounds for action puck, scrolling, and typing" && git push origin main

Done! The new haptic sounds (the clink, the scroll tick, and the typing sound), along with the logic driving them, have all been pushed up to the main branch.

Vercel should be deploying the updates to your live app as we speak! Is there anything else you'd like to tweak or add?

This is something stupid

I don’t know what is happening here. Kuch bhi ho raha. This is so stupid. But it’s fun.

Ran command: git statusRan command: git add src/app/components/ActionPuck.tsx src/app/components/NoteEditor.tsx public/sounds/clink.wav public/sounds/tick.wav public/sounds/type.wav && git commit -m "Add haptic UI sounds for action puck, scrolling, and typing" && git push origin main

Done! The new haptic sounds (the clink, the scroll tick, and the typing sound), along with the logic driving them, have all been pushed up to the main branch.

Vercel should be deploying the updates to your live app as we speak! Is there anything else you'd like to tweak or add?

this to → this. I don’t know, man. I need to do something. This is pretty boring. Today is Friday.