Skip to main content
RapidDev - Software Development Agency
App Featuresauth-security21 min read

How to Add File Encryption to Your App (Copy-Paste Prompts Included)

True file encryption means encrypting in the browser before upload — the server never sees plaintext. Use the Web Crypto API (browser-native, free) with AES-256-GCM, derive the key from the user's password via PBKDF2 (100,000 iterations), store only the salt and IV in your database, and upload the ciphertext blob to a private Supabase Storage bucket. Build time is 4–8 hours with V0; $0–$25/month running cost. The critical rule: if your encryption code runs in a server component, Edge Function, or API route, you do not have client-side encryption.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Advanced

Category

auth-security

Build with AI

4–8 hours with Lovable or V0

Custom build

2–3 weeks custom dev

Running cost

$0–$25/mo (Supabase Storage; Web Crypto API is free and browser-native)

Works on

Web

Everything it takes to ship File Encryption — parts, prompts, and real costs.

TL;DR

True file encryption means encrypting in the browser before upload — the server never sees plaintext. Use the Web Crypto API (browser-native, free) with AES-256-GCM, derive the key from the user's password via PBKDF2 (100,000 iterations), store only the salt and IV in your database, and upload the ciphertext blob to a private Supabase Storage bucket. Build time is 4–8 hours with V0; $0–$25/month running cost. The critical rule: if your encryption code runs in a server component, Edge Function, or API route, you do not have client-side encryption.

What File Encryption Actually Is

File encryption in an app context means the file is scrambled before it ever leaves the user's device — not encrypted in transit (HTTPS handles that), and not encrypted at rest by the storage provider (that protects against cloud provider breaches, not application-level data access). True end-to-end file encryption means your server, your database, your Supabase Storage bucket, and your own admin account see only binary noise. The Web Crypto API (built into every modern browser) provides AES-256-GCM encryption that is industry standard. PBKDF2 derives the AES key from the user's password with a random salt so the key never exists anywhere outside the user's session. The product decisions are what happens when a user forgets their password (all files become permanently inaccessible unless you implement key escrow), and whether files can be shared with other users (requires public key cryptography, significantly more complex).

What users consider table stakes in 2026

  • Transparent encryption — user uploads a normal file using a file input and downloads it decrypted automatically, with no manual key management visible
  • No perceptible performance degradation for files under 50MB — AES-GCM encryption of a 10MB file takes under 200ms in modern browsers
  • Encrypted files completely unreadable to anyone viewing the Supabase Storage bucket, including app admins — not just protected by access controls
  • Lock icon or 'Encrypted' badge on each file in the file list so users know their files are protected
  • Clear warning when the user sets their password: 'If you forget your password, your files cannot be recovered. There is no reset option.'
  • Key management section in account settings showing salt and backup options (if implemented)

Anatomy of the Feature

Six components implement end-to-end file encryption. Three of them involve cryptographic operations that AI tools frequently generate incorrectly — the anatomy section names the exact failure modes.

Layers:UIDataBackend

Client-side encryption layer

Backend

crypto.subtle.encrypt('AES-GCM', key, fileArrayBuffer) encrypts the file entirely in the browser. The result is an ArrayBuffer of ciphertext. A unique 12-byte IV (initialization vector) is generated per file using crypto.getRandomValues(new Uint8Array(12)) before every encryption call. The IV is prepended to the ciphertext before upload so it can be separated during decryption.

Note: crypto.subtle is only available in secure contexts (HTTPS or localhost) and browser environments. It must never be called in a Server Component, API route, Edge Function, or any server-side context — placing it there means plaintext files pass through your server before encryption, defeating the purpose.

Key derivation via PBKDF2

Backend

crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, passwordKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) derives a 256-bit AES key from the user's password and a random 16-byte salt. The key is not exportable (false in the extractable parameter) and never stored anywhere. The salt is stored in the encrypted_files table and used at decryption time to re-derive the same key from the same password.

Note: 100,000 PBKDF2 iterations is the NIST-recommended minimum as of 2023. Lower iteration counts make brute-force attacks against the salt viable if the database is compromised. The salt must be unique per user (or per file for maximum isolation) — never reuse a salt.

Encrypted file upload

Backend

After encryption, the ciphertext ArrayBuffer is converted to a Uint8Array, a 12-byte IV is prepended (first 12 bytes = IV, remaining bytes = ciphertext), and the combined Uint8Array is uploaded to a private Supabase Storage bucket as a .enc file using supabase.storage.from('encrypted-files').upload(). Supabase Storage receives only the binary ciphertext — it cannot read the file contents.

Note: Prepending the IV to the ciphertext before upload is simpler than storing them separately, as long as you document the layout. At download time, split on byte 12: iv = encryptedData.slice(0, 12); ciphertext = encryptedData.slice(12).

File metadata table

Data

A Supabase table stores the per-file metadata needed for decryption: original_filename, original_mime_type, salt (base64-encoded), storage_path, and file_size_bytes. The IV is not stored separately because it is prepended to the ciphertext in storage. RLS restricts all access to the file owner only.

Note: Never store the AES key, the user's password, or the derived key material in this table. The table is plaintext-readable by anyone with database access — it must contain only the minimum information needed to re-derive the key and locate the file in storage.

Decryption download flow

UI

When the user clicks download: 1) Fetch a signed URL for the .enc file from Supabase (60-second expiry). 2) Download the binary blob. 3) Split the ArrayBuffer into IV (first 12 bytes) and ciphertext (rest). 4) Re-derive the AES key from the user's password + the stored salt via PBKDF2. 5) crypto.subtle.decrypt() the ciphertext. 6) Create a Blob from the plaintext ArrayBuffer with the original MIME type. 7) Use URL.createObjectURL() and a temporary anchor element to trigger a browser download with the original filename.

Note: The decrypted plaintext exists only in browser memory during step 5–7 and is never sent to the server. If decryption fails (wrong password, corrupted file), crypto.subtle.decrypt() throws a DOMException — catch it and show a user-friendly error, not a raw exception.

Encrypted storage bucket

Backend

A private Supabase Storage bucket with an RLS policy: only the file owner (matching user_id in the encrypted_files table) can GET or DELETE the object. Signed URLs are generated server-side (Edge Function or API route) using the service role key and expire in 60 seconds to prevent URL sharing from granting permanent access.

Note: Set bucket visibility to 'private' in Supabase Dashboard → Storage → Buckets. Never use a public bucket for encrypted files — even though the content is ciphertext, public accessibility defeats defense in depth and violates reasonable security expectations.

The data model

One table stores file metadata. Run in the Supabase SQL editor (Dashboard → SQL Editor → New Query):

schema.sql
1create table public.encrypted_files (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid references auth.users(id) on delete cascade not null,
4 original_filename text not null,
5 original_mime_type text not null default 'application/octet-stream',
6 salt text not null, -- base64-encoded 16-byte PBKDF2 salt
7 storage_path text not null, -- path in the 'encrypted-files' Supabase Storage bucket
8 file_size_bytes bigint,
9 uploaded_at timestamptz not null default now()
10);
11
12alter table public.encrypted_files enable row level security;
13
14create policy "Users can view own file metadata"
15 on public.encrypted_files for select
16 using (auth.uid() = user_id);
17
18create policy "Users can insert own file metadata"
19 on public.encrypted_files for insert
20 with check (auth.uid() = user_id);
21
22create policy "Users can delete own file metadata"
23 on public.encrypted_files for delete
24 using (auth.uid() = user_id);
25
26-- No UPDATE policy: encrypted files are immutable once written
27-- To re-encrypt, delete and re-upload
28
29create index encrypted_files_user_idx
30 on public.encrypted_files (user_id, uploaded_at desc);
31
32-- Supabase Storage bucket RLS (set in Dashboard Storage Policies):
33-- CREATE POLICY "Owner access only"
34-- ON storage.objects FOR SELECT
35-- USING (bucket_id = 'encrypted-files' AND auth.uid()::text = (storage.foldername(name))[1]);

Heads up: The storage bucket must be created in Supabase Dashboard → Storage → Create Bucket with 'Private' selected. File paths in storage_path should include the user_id as a prefix folder (e.g., '{user_id}/{file_id}.enc') so the Storage RLS policy can restrict access by folder name.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

The only path for production-grade end-to-end encryption that passes a security audit. Full implementation with PBKDF2 key derivation, AES-GCM per-file IV, private Storage bucket, RLS, key rotation flow, and download decrypt pipeline.

Step by step

  1. 1Implement the encryption utility as a dedicated TypeScript module (lib/crypto.ts) with named functions: deriveKey(), encryptFile(), decryptFile() — never inline crypto calls in components
  2. 2Write unit tests for the crypto module covering: successful encrypt/decrypt roundtrip, wrong password failure, empty file, large file (100MB), corrupted ciphertext
  3. 3Implement a key rotation flow: re-encrypt existing files when user changes their password (derive new key, decrypt each file with old key, re-encrypt with new key, update storage_path)
  4. 4Add a key escrow option for enterprise use: encrypt the AES key itself with a recovery public key held by the organization, store the encrypted key escrow blob in the DB
  5. 5Security review before launch: confirm crypto.subtle never appears in server-side code, all bucket objects are private, signed URL expiry is short, and PBKDF2 iterations meet current NIST guidelines

Where this path bites

  • Key rotation for existing files requires re-downloading, decrypting, re-encrypting, and re-uploading every file — expensive for users with large storage
  • Multi-user file sharing requires public key cryptography (Elliptic Curve Diffie-Hellman or RSA-OAEP) to encrypt the AES key per recipient — a significant additional implementation

Third-party services you'll need

The encryption itself is entirely free and browser-native. Only storage has a cost:

ServiceWhat it doesFree tierPaid from
Web Crypto APIAES-256-GCM encryption and decryption, PBKDF2 key derivation — runs entirely in the browserBrowser-native, free, no API key neededFree
Supabase StoragePrivate bucket for encrypted file objects (.enc blobs) with RLS-controlled signed URL access1GB storage free; 50MB file size limit on free tier$25/mo Pro includes 100GB storage; additional $0.021/GB (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier (1GB storage). Encryption is free and runs on-device. At 100 users uploading modest files, 1GB is typically sufficient.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

AI moves encryption to the server side

Symptom: This is the most dangerous failure mode and the most common. Lovable and V0 sometimes generate the encryption logic inside a Supabase Edge Function or a Next.js API route — perhaps because the prompt mentioned 'secure upload'. When encryption runs server-side, the plaintext file is transmitted to the server over HTTPS, encrypted there, then stored. This provides no protection against a server breach or an admin viewing files.

Fix: Review every line of generated code involving crypto.subtle. It must appear inside a useEffect, a click event handler, or a browser-side utility function — never in files in app/api/, supabase/functions/, or any file without 'use client'. If you see crypto.subtle in a server file, that is a critical defect regardless of what the tests show.

ArrayBuffer and Uint8Array confusion silently corrupts files

Symptom: Web Crypto API methods return ArrayBuffer, but Supabase Storage upload expects a Blob or Uint8Array. File readers return ArrayBuffer or string. The spread operator [...new Uint8Array(buffer)] works but is extremely slow for large files. Mixing these types — or calling Buffer.from() in a browser context where Buffer is undefined — results in a corrupted upload that decrypts to garbage or throws DOMException.

Fix: Use explicit, documented type conversions: new Uint8Array(arrayBuffer) to convert ArrayBuffer to Uint8Array for upload. For combining IV and ciphertext: const combined = new Uint8Array(12 + ciphertext.byteLength); combined.set(iv, 0); combined.set(new Uint8Array(ciphertext), 12). Never use Buffer in browser code.

IV not stored or reused across files

Symptom: AES-GCM is catastrophically insecure when the same key and IV are used to encrypt two different files — an attacker who has both ciphertexts can recover the plaintext of both. AI tools sometimes generate a single static IV stored as a constant, or store the IV in a way that gets reused when the file metadata row is copied. A reused IV also makes the stored ciphertext manipulable without knowing the key.

Fix: Generate a fresh IV for every single file encryption call: crypto.getRandomValues(new Uint8Array(12)). Prepend the IV to the ciphertext before upload so they are always stored together. The encrypted_files table schema above does not store the IV as a separate column for this reason — it is inseparable from its ciphertext in the .enc blob.

Supabase Storage bucket left public by default

Symptom: Both Lovable and V0 default to public Storage buckets for convenience during initial setup. A public bucket means anyone with the URL of a .enc file can download the ciphertext. While they cannot decrypt it without the key, public access violates defense in depth, exposes file metadata through the URL path, and may violate HIPAA or GDPR requirements for data at rest.

Fix: Create the Storage bucket as 'Private' in Supabase Dashboard → Storage → Create New Bucket. Uncheck 'Public Bucket'. Generate signed URLs server-side with a 60-second expiry via an API route using the service role key. Add a Storage RLS policy restricting GET to the file owner based on the user_id prefix in the storage path.

Key lost when user forgets their password

Symptom: PBKDF2 derives the AES key from the password + salt. The key is never stored. If the user resets their Supabase Auth password (via the email reset flow), their old password is gone — and with it, the only way to derive the key for their previously encrypted files. All existing encrypted files become permanently inaccessible. This is by design for true zero-knowledge encryption, but most founders don't realize it until a user complains.

Fix: Choose one of two approaches before launch: 1) Zero-knowledge (no recovery): show a prominent, permanent warning on first encryption that 'If you reset your password, encrypted files cannot be recovered. Save your password somewhere safe.' 2) Key escrow: derive the AES key, encrypt it with a recovery key held by your organization or a KMS, and store the encrypted key escrow blob in the DB. Decision must be made before any files are encrypted — migrating between models requires re-encrypting all files.

Best practices

1

Always confirm crypto.subtle calls appear in browser-only code (client components, event handlers) — never in server files, no matter how convenient it looks

2

Generate a unique IV for every single file encryption call using crypto.getRandomValues(new Uint8Array(12)) — reusing IVs with AES-GCM is a catastrophic, silent security failure

3

Use 100,000 PBKDF2 iterations at minimum — lower counts make brute-force attacks against the stored salt practical if the database is breached

4

Never store the AES key, the password, or any key material in the database — only the salt (for key re-derivation) and the IV (if not prepended to ciphertext)

5

Set the Supabase Storage bucket to private and generate short-lived signed URLs (60 seconds) server-side — never expose long-lived or public links to encrypted blobs

6

Show a clear, permanent warning at first use that forgetting the encryption password means permanent file loss — make this impossible to miss, not buried in help text

7

Test the full encrypt-upload-download-decrypt cycle with at least three file types (PDF, image, zip) and confirm decryption fails gracefully with a user-friendly message on wrong password

8

Verify the encryption is actually working by viewing the raw .enc file in Supabase Storage and confirming it shows binary noise, not recognizable file headers

When You Need Custom Development

Client-side AES-256-GCM encryption with AI tools is achievable for a single-user file vault. Custom development is required when your encryption requirements expand:

  • You need zero-knowledge architecture with formal verification — not just client-side encryption, but a cryptographic proof that your team cannot access file contents under any circumstance
  • You need compliance with HIPAA Security Rule, GDPR Article 32, or SOC 2 Type II for encrypted PHI or PII at rest — which requires documented key management procedures, not just working code
  • You need key rotation for existing encrypted files — re-encrypting all files when a user changes their password is a significant engineering problem at scale
  • You need multi-user file sharing where multiple users can decrypt the same encrypted file — requires public key cryptography (ECDH or RSA-OAEP) to encrypt the file's AES key per recipient

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Does encrypting files affect upload speed?

Minimally for files under 50MB. AES-GCM encryption of a 10MB file takes 50–150ms in modern browsers — imperceptible to users. Files over 100MB may take 1–3 seconds to encrypt before upload begins, which is worth showing a progress indicator for. PBKDF2 key derivation with 100,000 iterations takes 200–500ms and happens once per session (or per upload if you re-derive per file).

What happens to my files if I forget my password?

With true client-side encryption using PBKDF2, the AES key is derived from your password. If you forget your password and reset it via email, your old password is gone — and with it, the ability to re-derive the key. All previously encrypted files become permanently inaccessible. This is the correct behavior for zero-knowledge encryption. If this is unacceptable for your users, you need to implement key escrow: encrypt the AES key with a recovery key and store the encrypted copy separately.

Can admins read encrypted files?

No — that is the point of client-side encryption. Admins see only the .enc binary blob in Supabase Storage. Without the user's password (which is never stored), there is no way to derive the AES key and decrypt the file. This is different from 'storage encryption at rest', where the cloud provider encrypts data but can still access it on behalf of the app operator.

Is AES-256 encryption safe enough for sensitive documents?

AES-256-GCM is the industry standard for symmetric encryption and is approved by NIST for protecting classified information up to TOP SECRET. The algorithm itself is not the risk — the implementation details are. A correct AES-256-GCM implementation with unique IVs and proper PBKDF2 key derivation is secure. The failures come from: reused IVs, weak PBKDF2 iteration counts, or encryption happening server-side instead of client-side.

Does file encryption affect search and preview features?

Yes, fundamentally. Encrypted files are binary blobs — your server cannot index their text content for search, generate thumbnails, or display previews without the decryption key. If you need full-text search of encrypted documents, you must decrypt the file client-side before display and never store plaintext server-side. This makes encrypted files incompatible with server-side search, PDF preview, or image thumbnail features.

What's the difference between encryption at rest and end-to-end encryption?

Encryption at rest (like Supabase Storage's default) means the cloud provider encrypts the file on their disk. They hold the keys, so they (or someone with access to their systems) can decrypt it. End-to-end encryption means only the intended recipient (the user) holds the key. Your server never has the key and cannot decrypt the file regardless of who asks. The Web Crypto API approach on this page is end-to-end encryption.

Can I share encrypted files with other users?

Not with symmetric encryption alone. If the AES key is derived from User A's password, User B cannot decrypt it without knowing User A's password. Sharing requires asymmetric cryptography: generate a key pair for each user, encrypt the file's AES key with the recipient's public key (using ECDH or RSA-OAEP), and store that encrypted copy. The recipient uses their private key to recover the AES key and decrypt the file. This is significantly more complex to implement correctly.

How do I know my files are actually encrypted?

Upload an encrypted file and then view it directly in the Supabase Dashboard → Storage → your bucket. The raw bytes should look like random binary garbage, not a recognizable file header. For a JPEG, you should NOT see 'JFIF' or 'Exif' in the first few bytes. For a PDF, you should NOT see '%PDF-'. If you can see recognizable file content in the raw Storage view, your encryption is not happening before upload.

RapidDev

Need this feature production-ready?

RapidDev builds file encryption into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.