
The project
A real-time chat application combining browser-encrypted message storage and image uploads with WebRTC voice and video calls. Each payload receives its own AES-256-GCM key, wrapped for both participants with RSA-OAEP. Firestore coordinates conversation updates and call signaling, while password-encrypted key backups support access from additional devices.
How it works
Unlock the conversation
The browser loads local keys or decrypts a password-protected backup when using another device.
Encrypt before sending
Text and image payloads are encrypted on the device, with their encryption key wrapped for both participants.
Receive in real time
Firestore synchronizes the conversation; the recipient decrypts stored payloads locally and sees typing and delivery updates.
Start a call
Call signaling establishes a WebRTC audio or video connection from the same conversation.
Engineering decisions
Encrypt messages on the device
The challenge
Stored message and image payloads need encryption before they reach the database or object storage.
The approach
The browser creates a fresh AES-GCM key for each payload and wraps it separately for sender and recipient with RSA-OAEP. Recipients decrypt stored payloads locally. Notifications and link previews remain outside this encryption boundary.
Recover keys across devices
The challenge
A user needs to open existing conversations from another device without uploading a plaintext private key.
The approach
Private keys are kept locally in IndexedDB and backed up to an owner-scoped Firestore record after password-based encryption with PBKDF2 and AES-GCM.
Keep conversations in sync
The challenge
Both participants need timely messages, typing indicators, delivery receipts, and incoming call updates.
The approach
Used participant-scoped real-time updates to coordinate conversations and establish audio and video calls.
Architecture
Encrypted storage. A separate path for calls.
Browsers encrypt stored message and image payloads. Firestore coordinates delivery and WebRTC call signaling.
Encrypt here. Decrypt there.
Browser encryption for stored messages and images
Encrypt the payload
A fresh AES-GCM key encrypts each payload. RSA-OAEP wraps the key for each participant.
Next.js
Store ciphertext
Firestore stores encrypted message payloads; Cloud Storage holds encrypted image files.
Firestore
Cloud Storage
Unlock & read
The recipient’s private key unwraps the payload key. Decryption happens locally.
Next.js
Private keys are kept locally, with password-encrypted backups in Firestore for use on additional devices. Push notifications and link previews use server-side processing outside the encrypted storage boundary.
Calls take a separate route
Signaling coordinates; WebRTC carries media
Call signaling
Exchanges offers, answers and ICE candidates. Also synchronizes presence, typing and receipts.
Firestore
Identity & delivery
Passkey sign-in, media upload/download, push notifications and ICE server credentials.
Next.js
Firebase
A standalone Next.js service provides the app and APIs; Firebase supports identity and live state.
Outcome
Stored message and image payloads reach the server already encrypted. Each payload uses a fresh AES-256-GCM key, wrapped separately for the sender and recipient, and private-key backups are password-encrypted for recovery on another device. Real-time updates and WebRTC calls share the conversation interface; server-processed notifications and link previews remain outside the stored-payload encryption boundary.