Building a Secure Full-Stack Chat Application

5 min read

I set out to build a personal chat application with security as a core design principle, not an afterthought. I wanted the backend and frontend to enforce best practices from the ground up, just like some of the other secure messaging apps out there.

Architecture

The backend is written in Node.js using Express. It serves as both a REST API and a WebSocket server for real-time messages. For authentication and user data, I used Firebase. Firebase Auth for securing user identity and Firebase Realtime Database for account info, metadata, and chat messages (now that I look back on it Firebase Firestore would have probably been a better choice). Google Cloud Storage handles user profile images, and Google Secret Manager secures sensitive configuration like session secrets. Winston manages all request and security logging.

The frontend is a custom-built SPA that communicates with the Express backend.

Authentication flow

Users sign in via Firebase Auth (email & password). The client obtains an ID token from Firebase, then sends it to the backend, which verifies it using the Firebase Admin SDK. The backend then creates an Express session, stores it in an httpOnly, Secure cookie (with SameSite=Strict), and delivers a CSRF token as both a cookie and header for the frontend to use in later requests.

Sign-up process

The sign-up flow was one area where I really wanted to avoid the usual “just trust the client” trap. So I built multiple layers of validation that all happen on the server.

When someone tries to create an account, the backend first checks whether the email and display name are already taken by querying Firebase. I didn’t want anyone sneaking in with a duplicate identity. Then comes the password validation - and this one is done entirely server-side, not just in the browser. I enforce a solid complexity rule: minimum length, at least one uppercase letter, one lowercase, one digit, and one special character. Only after all those checks pass does the account actually get created in Firebase Auth.

It feels a bit strict at first, but I’d rather frustrate a few users during signup than deal with weak accounts later.

Security measures

I tried to think about security at every layer instead of bolting it on at the end.

For CSRF protection, every state-changing request - POST, PUT, DELETE, file uploads, and even some sensitive GETs - requires a valid CSRF token. The server generates the token, stores it in the session, and sends it to the client both as a cookie and in a custom header. On every request, it checks that the header, cookie, and session token all line up. It’s a bit of extra work, but it closes off a pretty common attack vector.

I also added rate limiting using express-rate-limit across the board. There are separate limits for general routes, email-related actions, and file uploads. This helps throttle brute-force attempts, spam friend requests, and excessive message sending per IP.

Everything runs over HTTPS only. Cookies are set with Secure and SameSite=Strict. For WebSockets, I added multiple checks: IP restrictions, CSRF token validation during the initial handshake, and verification of the Firebase ID token. Once connected, each user generates a shared secret using ECDH, and we use HMAC to authenticate messages.

On top of that, Helmet sets strong security headers (including a tight Content Security Policy), disabling iframe embedding, and all user input - chat messages, profile data, everything - gets sanitized server-side to prevent XSS.

It’s definitely more defense-in-depth than most personal projects, but that was the whole point.

Features

The app ended up with a pretty clean set of features, all built with the security model in mind.

You can search for other users and check username availability in real time. Sending, accepting, or rejecting friend requests happens through secure REST endpoints. Once you’re friends, you can add or remove people from your list as expected.

The real highlight is the real-time chat. It runs over WebSockets that are properly authenticated with a secure handshake, and every message is protected with HMAC using per-user shared secrets.

You can also update your profile and upload a profile picture (only PNG or JPEG, max 8MB). I used Multer for handling the upload on the backend and enforced strict rules before saving anything to Google Cloud Storage.

Finally, I log pretty much everything for auditing: signups, logins, errors, friend requests, uploads, failed attempts - you name it. Winston handles all the logging so I can actually review what’s happening if something looks suspicious.

Source Code

I’m holding off on publishing the repository until I can separate the reusable parts of the app from the private infrastructure around it. A secure chat project has a lot of sharp edges: auth configuration, secret handling, Firebase rules, storage buckets, logging setup, and deployment assumptions all need to be documented carefully so the code is useful without exposing anything sensitive or encouraging a bad setup.

Conclusion

This project was both challenging and rewarding. It’s not a battle-tested, enterprise-grade chat platform, but it’s a solid proof of concept that demonstrates how to layer security into every part of a full-stack application. I learned a lot about CSRF protection, secure authentication flows, sanitization, HMACs, and the importance of defense-in-depth. There’s still plenty of room for improvement, and I’m confident that there’s vulnerabilities in the current implementation, but building this app gave me a much deeper appreciation for the complexities of secure software design.

Back to top