Talkify is a modern, real-time chat application featuring End-to-End Encryption (E2EE) built with React and Spring Boot. It uses WebSockets for instant message delivery and the Web Crypto API to ensure no one but the intended recipient can read your messages. The backend is performance-engineered with database optimizations and Redis caching for production-grade scalability.
- End-to-End Encryption (E2EE)
- Client-side cryptographic key generation using the Web Crypto API.
- Non-extractable ECDH (Curve P-384) private keys safely persisted in the browser's IndexedDB.
- Secure message payloads encrypted with AES-GCM (256-bit) using derived shared secrets.
- The backend server only ever handles ciphertext; it cannot read your messages.
- Real-Time Communication
- Low-latency bi-directional messaging powered by WebSockets and the STOMP protocol.
- JWT Authentication
- Secure user registration and login endpoints protected by Spring Security.
- Private 1-on-1 Chat Rooms
- Isolated chat histories tied securely between two users.
- Read Receipts & Statuses
- Live message statuses (Sent, Delivered, Read) complete with UI checkmarks.
- Database Performance Optimizations
- Composite indexing, cursor pagination, denormalization, and batch SQL operations.
- Redis Caching
- Sidebar data cached in Redis with automatic invalidation on new messages.
- Modern UI / UX
- Responsive, WhatsApp-like design built seamlessly with React, Tailwind CSS, and Lucide Icons.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend (React + Vite) โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ React UI โ โ STOMP.js โ โ E2EE Engine โ โ
โ โ (App.jsx)โ โ (WebSocket) โ โ (ECDH + AES-GCM) โ โ
โ โโโโโโโฌโโโโโโ โโโโโโโโฌโโโโโโโโ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ โ
โ โHTTP REST โSTOMP/WS โIndexedDB โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โ โผ โผ Spring Boot 4.0 Backend โ
โ โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ REST API โ โ WebSocket โ โ Security Layer โ โ
โ โ Controllersโ โ Controller โ โ (JWT + Spring Security) โ โ
โ โโโโโโโฌโโโโโโโ โโโโโโโโฌโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Service Layer โ โ
โ โ UserService ยท ChatMessageService (@Cacheable, @Transact) โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโผโโโโโโโโโโ โโโโโโโโโโโโผโโโโโโโโโโโ
โ PostgreSQL โ โ Redis Cache โ
โ (Primary Storage) โ โ (Sidebar Data) โ
โ โ โ โ
โ โข users โ โ โข user_chat_rooms:: โ
โ โข chat_rooms โ โ {username} โ
โ โข chat_messages โ โ TTL: 1 hour โ
โ โข chat_room_ โ โ โ
โ participants โ โ โ
โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
- Framework: React 19 with Vite
- Styling: Tailwind CSS
- Icons: Lucide React
- WebSockets:
@stomp/stompjs&sockjs-client - Security: Web Crypto API + IndexedDB
- Framework: Java 17 + Spring Boot 4.0
- Database: PostgreSQL
- Cache: Redis (via
spring-boot-starter-data-redis) - ORM: Spring Data JPA / Hibernate
- Security: Spring Security + JWT (
io.jsonwebtoken) - Messaging: Spring WebSocket (STOMP)
The backend implements 6 key strategies to minimize database load and ensure sub-millisecond response times:
| # | Strategy | Technique | Impact |
|---|---|---|---|
| 1 | Caching | Redis @Cacheable on sidebar getLastMessages() |
Sidebar loads from memory, 0 DB hits on repeat |
| 2 | Pagination | findTop50...OrderByTimestampDesc |
Never loads more than 50 messages per chat |
| 3 | Indexing | Composite indexes on (chat_room_id, timestamp) and (sender_id, status) |
O(log N) lookups instead of O(N) full table scans |
| 4 | Denormalization | last_message field directly on chat_rooms table |
Sidebar never touches chat_messages table |
| 5 | Batch Updates | UPDATE ... WHERE id IN (...) via @Modifying query |
50 read receipts = 1 query instead of 50 |
| 6 | @Transactional | Single DB session per message send | Eliminates redundant merge/re-fetch queries |
BEFORE: Opening a chat with 50 unread messages
โ 1 SELECT (all messages) + 50 SELECT + 50 UPDATE = 101 queries
AFTER: Opening a chat with 50 unread messages
โ 1 SELECT (top 50 paginated) + 1 SELECT (IDs only) + 1 UPDATE (bulk) = 3 queries
- Node.js (v18+)
- Java 17+
- PostgreSQL
- Docker (for Redis)
docker run -d --name chat-redis -p 6379:6379 redis- Navigate to the
backenddirectory:cd backend - Update the
src/main/resources/application.propertiesfile with your PostgreSQL credentials. - Run the Spring Boot application using Maven:
The backend will typically start on
./mvnw spring-boot:run
http://localhost:8080.
- Navigate to the
frontenddirectory:cd frontend - Install the JavaScript dependencies:
npm install
- Start the Vite development server:
The frontend will typically start on
npm run dev
http://localhost:5173.
When a user signs up or logs in:
- The client browser generates a public/private key pair (ECDH P-384).
- The private key is marked as
extractable: falseand saved permanently in the browser's IndexedDB. (Even XSS attacks cannot extract the raw private key material). - The public key is sent to the Spring Boot backend to be associated with the user account.
When User A messages User B:
- User A fetches User B's public key from the backend API.
- User A uses their own private key and User B's public key to derive a secure shared secret locally via ECDH.
- User A encrypts the plaintext message using AES-GCM with the derived shared secret.
- User A sends the resulting ciphertext to the server over WebSockets.
- User B receives the ciphertext, derives the exact same shared secret using their private key + User A's public key, and decrypts the message securely on their screen.
User A Server User B
โ โ โ
โ Generate ECDH keypair โ Generate ECDH keypair โ
โ Store private in IndexedDB โ Store private in IndexedDBโ
โ โ โ
โโโโโ Send public key โโโโโโโโโบโโโโโโ Send public key โโโโโโโโโ
โ โ โ
โ Derive shared secret โ โ
โ (myPrivate + theirPublic) โ โ
โ โ โ
โ Encrypt: AES-GCM(message) โ โ
โโโโโ "E2E:iv:ciphertext" โโโโโบโโโโโ "E2E:iv:ciphertext" โโโโโบโ
โ โ โ
โ Server CANNOT โ Derive shared secret โ
โ decrypt this! โ (myPrivate + theirPublic) โ
โ โ โ
โ โ Decrypt: AES-GCM(ct) โ
โ โ "Hello!" โ
โ
Building this project provided deep hands-on experience in several complex domains:
- Web Cryptography API: Leveraging browser-native APIs for secure key generation (ECDH), strict extraction rules, and symmetric encryption (AES-GCM), preventing exposure of private keys even if the JavaScript context is compromised.
- IndexedDB: Moving away from insecure
localStorageto securely storeCryptoKeyobjects natively without unnecessary string parsing overhead or exposing keys to XSS payloads. - Database Performance Engineering: Implementing composite indexing, cursor pagination, denormalization, batch SQL updates, and Redis caching to reduce database load by over 90%.
- Redis Caching with Spring Boot: Integrating
spring-boot-starter-data-redisfor cache-aside pattern, understanding JDK vs JSON serialization tradeoffs, and designing smart cache invalidation strategies. - WebSocket Concurrency & State Management: Managing complex race conditions in React where WebSocket connections occur simultaneously with REST API metadata fetches, ensuring message decryption never fails due to missing state.
- Full-Stack Integration: Bridging a robust Java + Spring Boot backend via STOMP WebSockets and REST endpoints with a modern React + Vite frontend seamlessly.
- Group Chats: Extend E2EE architecture using the Signal Protocol (Sender Keys) or by individually encrypting payloads for each group member.
- Push Notifications: Integrate service workers and Web Push API to alert users of messages when the app is running in the background.
- Media & Attachments: Apply client-side encryption to files (images, PDFs) before uploading them to an S3-compatible cloud storage bucket.
- Offline Mode: Implement local PouchDB/IndexedDB synchronization so users can view parsed chat histories even without an active internet connection.
- Kafka Event Streaming: Offload heavy secondary tasks (analytics, audit logging) to async Kafka consumers for further performance gains.