A journey from React basics to production deployment — lessons learned building a real-world social networking platform
The Problem
I wanted to learn full-stack development, but most tutorials felt disconnected from reality. Todo apps and weather widgets don't teach you about real-world challenges like:
- Managing complex database relationships (users, posts, comments, messages)
- Handling authentication securely at scale
- Dealing with Render's ephemeral filesystem destroying uploads
- Building responsive UIs that actually work on mobile
So I decided to build something ambitious: ConnectNow — a full-stack social media platform with posts, messaging, profiles, and real-time interactions.
The Architecture
ConnectNow has three independent pieces deployed separately:
Frontend (React + Vercel)
↓ HTTP
Backend (Node.js/Express + Render)
↓
Database (MongoDB Atlas)
Why separate frontend and backend?
- Decoupling – My backend can serve multiple clients (web, mobile, third-party integrations)
- Parallel development – Frontend and backend teams could work independently
- Independent scaling – If one part gets hammered with traffic, I can scale it without scaling the other
The Frontend Stack
- React.js with hooks for state management
- CSS3 for responsive design (mobile-first)
- React Router for navigation
- Deployed on Vercel for automatic deployments on every git push
The frontend is straightforward: it's just a single-page application that talks to the backend API. The complexity is in the interactions — real-time message updates, smooth theme switching, proper authorization checks.
The Backend Stack
- Node.js + Express.js for the REST API
- MongoDB for the database
- JWT for stateless authentication
- BCrypt for password hashing
- Google OAuth for social login
- Cloudinary for cloud image storage (this became crucial!)
- Deployed on Render for $0/month (free tier)
The Database Design
This was the most challenging part. A social media app has complex relationships:
A User can:
- Create many Posts
- Like many Posts
- Follow many other Users
- Send many Messages
- Have many Connections (followers)
A Post belongs to one User
- Can have many Likes
- Can have many Comments
- Can have one Image
A Message belongs to two Users (sender & receiver)
- Can be edited
- Can be deleted
I normalized the schema to avoid data duplication:
// Users collection
{
_id: ObjectId,
email: "user@example.com",
password: "hashed_with_bcrypt",
profile_picture: "cloudinary_url",
followers: [userId, userId, ...], // Array of follower IDs
following: [userId, userId, ...],
created_at: Date
}
// Posts collection
{
_id: ObjectId,
author: userId, // Reference, not embedded
title: "Amazing View",
description: "...",
image: "cloudinary_url",
likes: [userId, userId, ...],
comments: [commentId, commentId, ...],
created_at: Date
}
// Comments collection
{
_id: ObjectId,
post_id: postId,
author: userId,
text: "Very nice post!",
created_at: Date
}
// Messages collection
{
_id: ObjectId,
sender: userId,
recipient: userId,
text: "Hey! How are you?",
is_edited: false,
created_at: Date,
deleted_at: null
}
Key decision: I used arrays of IDs instead of embedding full documents. Why?
- Memory efficient — I'm not duplicating user data in every message
- Flexible queries — I can efficiently find "all posts liked by user X"
- Scalability — If I need to change a user's name, I update it once, everywhere
Challenges & Solutions
🚨 Challenge #1: Images Disappearing on Render
The Problem:
After deploying to Render's free tier, I noticed a nightmare: all uploaded images disappeared after an hour.
Render's free tier uses an ephemeral filesystem — any files you write are deleted when the dyno restarts. This is by design to save costs, but it broke my file upload system.
The Solution:
I switched to Cloudinary, a cloud image hosting service. Now:
- User uploads image → sent to Cloudinary
- Cloudinary returns a permanent URL
- That URL is stored in MongoDB
- Even if Render restarts, the image link persists
This was a learning moment: don't store files on servers that might restart. Use cloud storage (S3, GCS, Cloudinary, etc.).
// Before (broken):
const filename = `${Date.now()}_${req.file.filename}`;
fs.writeFileSync(`uploads/${filename}`, req.file.buffer); // ❌ Lost on restart
// After (works):
const result = await cloudinary.uploader.upload_stream(...);
const imageUrl = result.secure_url; // ✅ Permanent URL
🚨 Challenge #2: "Can't find module 'mongoose'"
The Problem:
Local development worked fine, but production (Render) crashed on startup: "Cannot find module 'mongoose'".
The Root Cause:
I forgot to commit node_modules/ (correctly — it's in .gitignore). But I also didn't commit package-lock.json — so when Render ran npm install, it pulled slightly different versions that were incompatible.
The Solution:
Always commit package-lock.json. This ensures everyone (including your deployment platform) uses the exact same dependencies.
git add package-lock.json
git commit -m "Add package-lock for reproducible builds"
🚨 Challenge #3: CORS Errors When Frontend Calls Backend
The Problem:
Access to XMLHttpRequest at 'https://connectnow-backend.onrender.com/...'
from origin 'https://connect-now-bice.vercel.app' has been blocked by CORS policy
My frontend couldn't talk to my backend because of Cross-Origin Resource Sharing (CORS) restrictions.
The Solution:
Configure CORS on the backend to allow requests from the frontend domain:
const cors = require('cors');
app.use(cors({
origin: 'https://connect-now-bice.vercel.app', // Only allow this domain
credentials: true // Allow cookies for auth
}));
Security lesson: Never use cors({ origin: '*' }) in production — that's like leaving your front door open. Whitelist only the domains you trust.
Technical Wins
✅ Real-Time Messaging
Users can send messages, and the UI updates instantly. I achieved this with a simple polling strategy:
- Frontend fetches messages every 500ms
- Backend returns only new messages since last fetch
- This is lighter than WebSockets for a small app
For a production app with millions of users, I'd use WebSockets or Firebase Realtime Database, but polling works great for learning.
✅ Secure Authentication
Users log in with email/password or Google OAuth. Here's how I kept it secure:
// 1. Hash passwords before storing
const hashedPassword = await bcrypt.hash(password, 10);
await User.create({ email, password: hashedPassword });
// 2. Verify password on login
const isValid = await bcrypt.compare(inputPassword, storedHash);
// 3. Issue JWT token
const token = jwt.sign({ userId }, 'SECRET_KEY', { expiresIn: '7d' });
// 4. Require token for protected routes
app.get('/api/profile', authenticateToken, (req, res) => {
// Only authenticated users reach here
});
✅ Password Reset Flow
I implemented a proper email-based password reset:
- User clicks "Forgot Password"
- Backend generates a unique reset token (valid for 15 minutes)
- Email is sent with reset link
- User clicks link → sets new password
- Token is invalidated
This is much better than "security questions" or "call customer support".
What I Learned
1. Database Design is 80% of the Work
Most complexity in backends comes from the data model. Getting schema relationships wrong early means painful refactoring later.
2. Deployment is Not the End, It's the Beginning
The hardest bugs happen in production, not local development. I had to debug:
- Why images disappeared (Render's filesystem)
- Why auth tokens weren't persisting (CORS cookies)
- Why messages weren't syncing (MongoDB connection pooling)
3. Security Requires Constant Vigilance
One small mistake (like hardcoding API keys in frontend code) can compromise everything. I learned to:
- Use environment variables for secrets
- Validate input on the backend (never trust the client)
- Hash passwords, don't store plaintext
- Use HTTPS everywhere
4. Your Database is Your Bottleneck
As I added features, every page load was making 10+ database queries. I learned to:
- Use database indexes on frequently queried fields
- Combine queries where possible
- Cache results that don't change often
Deployment Checklist
By the end, here's what a proper deployment looked like:
- ✅ Frontend built and minified
- ✅ Environment variables set on deployment platform
- ✅ Database migrations run
- ✅ CORS configured for production domain
- ✅ SSL/HTTPS enforced
- ✅ Error logging set up (Sentry, LogRocket, etc.)
- ✅ API rate limiting enabled
The Result
ConnectNow is now live at https://connect-now-bice.vercel.app.
You can:
- Create an account (or test with
test@example.com) - Create posts with images
- Like, comment, and share
- Send messages to friends
- Search for new users
- Toggle dark/light mode
The app handles real-time interactions, secure authentication, image uploads, and responsive design — all the core skills needed for production full-stack development.
What's Next?
If I were to continue this project, I'd add:
- WebSockets for true real-time messaging
- Push notifications for new messages
- Video calling (WebRTC)
- Post analytics (who viewed your posts)
- Content moderation (flagging inappropriate posts)
- Performance monitoring (understand bottlenecks)
But for now, ConnectNow demonstrates the fundamentals: how to design, build, deploy, and maintain a real-world full-stack application.
Key Takeaways for Aspiring Full-Stack Developers
Start with a real problem, not a tutorial. Building something you care about keeps you motivated through the hard parts.
Get to deployment early. Bugs that only appear in production teach you things localhost never will.
Security isn't optional. Treat it as a core feature from day one, not an afterthought.
Database design matters more than framework choice. Spend time getting the schema right.
Ship imperfect code. You learn more from a deployed app with 100 bugs than a perfect local app with zero users.
Have you built a full-stack app? What was your biggest challenge? Drop a comment below — I'd love to hear about it.
Happy building! 🚀
ConnectNow source code: https://github.com/smithayenugu/connectNow
Top comments (0)