How a team of four backend developers built a real-time, end-to-end encrypted messaging app in 3 weeks, exploring the architectural decisions, technical challenges, and trade-offs involved in creating a secure communication platform.
Building an End-to-End Encrypted Messaging Platform: Architecture Challenges and Trade-offs

In the fast-evolving landscape of communication technology, building a secure messaging platform presents unique challenges that extend far beyond standard application development. When a team of four backend developers set out to build an end-to-end encrypted messaging app in just three weeks, they discovered that encryption isn't just a feature—it fundamentally reshapes how you approach system architecture.
The Vision: Privacy as Foundation
The project began with a clear but ambitious vision: create something akin to WhatsApp, with privacy as the foundational principle. Messages had to be encrypted on the sender's device and decrypted only on the recipient's device, ensuring that even the service providers couldn't access message contents.
This requirement immediately distinguished the project from typical CRUD applications. "Building a simple CRUD app is relatively easy, REST APIs, maybe a real-time feature here and there. E2EE is a different ball game entirely," the team lead noted. "It forces you to think about different boundaries, what does the server know? What should it know? And what is off limit?"
The Tech Stack: Balancing Familiarity and Requirements
The team's technology choices reflected pragmatic considerations:
Node.js with TypeScript: TypeScript in strict mode was non-negotiable. With four developers writing backend code simultaneously, type safety ensured consistency across the codebase. Every API response shape, socket event payload, and database model was fully typed, catching potential mismatches at compile time.
Express.js: Despite considering alternatives, Express was the obvious choice due to team familiarity. The team built a clean middleware stack with validation, JWT authentication, error handling, and request logging.
PostgreSQL with Knex: Relational databases fit the domain well, with clear relationships between users, conversations, members, and messages. Knex provided type-safe migrations and query building without the "magic" of a full ORM.
Socket.io with Redis adapter: For real-time messaging, Socket.io's room abstraction mapped perfectly to chat conversations. The Redis adapter enabled horizontal scaling, allowing messages to reach users connected to different server instances.
BullMQ: For offline message delivery and push notifications, BullMQ provided job queuing with automatic retries and exponential backoff.
Firebase and Cloudinary: Firebase handled authentication and push notifications, while Cloudinary managed media with its transformation pipeline.
The Architectural Cornerstone: Server as Storage Only
Early in week one, the team made a decision that would shape everything that followed: the server would store ciphertext and nothing else. This seemingly obvious requirement for an E2EE app had profound implications:
- The backend contained no business logic dependent on message content
- Encryption and decryption occurred entirely on the client
- Database breaches would only yield encrypted, unreadable blobs
- The server genuinely could not comply with requests to hand over message contents
The Encryption Challenge: Web Crypto API vs. Signal Protocol
The team initially planned to use the Signal Protocol on both frontend and backend. However, they encountered a significant obstacle: libsignal, the Node.js implementation of the Signal Protocol, uses native bindings that don't work in browsers.
This forced a pivot to the Web Crypto API, which:
- Is built into all modern browsers
- Has no external dependencies
- Offers non-extractable key storage in IndexedDB
The implementation used ECDH for key agreement and AES-256-GCM for symmetric encryption. Private keys were stored as non-extractable CryptoKey objects in IndexedDB, ensuring they could only be used for cryptographic operations, not extracted.
The message flow followed this pattern:
- Frontend fetches recipient's public key from backend
- Generates ephemeral key pair for the message
- Uses ECDH to derive shared secret
- Encrypts message with AES-256-GCM using the shared secret
- Sends encrypted blob to backend
- Backend stores it without reading it
- Recipient's frontend derives same shared secret and decrypts locally
The Three-Week Development Sprint
The team structured their work across three focused weeks:
Week 1: Foundation
- Express scaffold setup
- TypeScript configuration
- Database migrations
- Firebase Auth integration
- JWT session management
- Encryption key management infrastructure
The primary focus was authentication. "The auth middleware was everything," the team lead explained. "Everything else depended on it." They implemented Firebase Auth for phone number verification and OTP, then exchanged Firebase ID tokens for internal JWT pairs with 15-minute access tokens and 30-day refresh tokens.
Week 2: Core Product
- Socket.io server with Redis adapter
- Message routing
- Message persistence
- Delivery status tracking
- Group messaging
- Media pipeline
- Push notifications
This proved the most challenging week, requiring simultaneous consideration of multiple system states: socket connection registry in Redis, message status in PostgreSQL, optimistic UI on frontend, and BullMQ queue for offline delivery.
Week 3: Integration The final week focused on connecting frontend and backend. The TypeScript choice proved particularly valuable here, with type definitions ensuring most integrations "just worked" and specific, traceable errors where they didn't.
Challenges Encountered
Despite their success, the team faced several significant challenges:
Authentication Flow Complexity Finalizing and understanding the authentication flow, particularly integrating Firebase auth with their custom JWT system, proved more complex than anticipated.
The libsignal Mismatch The team spent considerable time building backend key validation assuming the frontend would use Signal Protocol. When they discovered libsignal's browser limitations, they had to strip out validation and rethink their encryption approach.
Express Route Ordering
A subtle but critical mistake with parameterized routes in Express caused unexpected behavior. Routes like /:id would swallow everything that came after them if registered in the wrong order (e.g., GET /users/level-up must come before GET /users/:id).
Lessons Learned
Reflecting on the project, the team identified several areas for improvement:
Shared Type Contracts Starting with a shared type contract between frontend and backend would have prevented integration issues. "A shared type contract would have helped avoid some of the issues we faced while building and integrating the front and backend, as it ensures both sides are always on the same page about how data is sent and received."
Dependency Audits Better understanding of parallel versus blocked tasks would have improved development planning.
Stack Validation "Proper research on stack choice" was identified as crucial. "This is definitely a hard check for me from now on, even when it feels like you're using the industry standard. It's always best to ensure what you go with aligns perfectly with your goals and works properly with all other tools being used in the project."
Future Directions
The current implementation provides solid encryption but lacks forward secrecy. The team plans to implement the Signal Protocol's Double Ratchet algorithm, which ensures that if a key is compromised, past messages remain safe.
They're also preparing to extend the platform to mobile using React Native, as the current backend is designed to be client-agnostic. Voice and video calls are planned for a future phase, leveraging their existing signaling infrastructure with WebRTC for peer-to-peer connections.
The Core Architectural Principle
The most significant lesson from this project was the importance of establishing constraints early. "Make the hard architectural decisions first. For us, the decision that the server would never read message content was not negotiable. Everything else followed from it. Start with your constraints, not your components."
Building a secure messaging platform in three weeks was ambitious, but by establishing clear boundaries and making deliberate architectural choices, the team created a functional system that truly protects user privacy. The code may have rough edges, and the UI could improve, but the core achievement remains: messages encrypted on one device are decrypted on another without the server ever knowing what was said.
As communication continues to evolve, such projects demonstrate that privacy isn't just about adding encryption—it requires rethinking fundamental assumptions about system architecture and data boundaries.

Comments
Please log in or register to join the discussion