A full-stack social networking web application where users can connect, follow, post, share stories, and chat in real time.
- Overview
- Live Demo
- Features
- Tech Stack
- Project Structure
- Database Schema
- API Reference
- Background Jobs (Inngest)
- Getting Started
- Deployment
- How Key Features Work
LoopIn is a LinkedIn/Instagram-inspired social networking platform built with the MERN stack. It supports user authentication via Clerk, real-time messaging using Server-Sent Events (SSE), cloud image storage via ImageKit, automated background jobs with Inngest, and email notifications via Nodemailer + Brevo SMTP.
| Service | URL |
|---|---|
| Frontend | Deployed on Vercel |
| Backend | Deployed on Vercel (serverless) |
- 🔐 Authentication — Secure sign-up/sign-in powered by Clerk (JWT-based)
- 👤 User Profiles — Update name, bio, location, profile picture, and cover photo
- 📰 Posts & Feed — Create text, image, or mixed posts; like/unlike posts
- 📸 Stories — 24-hour disappearing stories (text, image, or video); auto-deleted by background job
- 🔗 Connections — Send/accept connection requests with rate limiting (max 20 per 24 hours)
- 👥 Follow System — Follow/unfollow users independently of connections
- 💬 Real-Time Chat — One-on-one messaging using Server-Sent Events (SSE), with image support
- 🔔 Notifications — In-app toast notifications for incoming messages; email notifications for connection requests and unseen messages
- 🔍 Discover Users — Search by name, username, email, or location
- 📧 Email Reminders — Automated daily digest of unseen messages; 24-hour connection request reminders
| Package | Version | Purpose |
|---|---|---|
| React | 19.x | UI framework |
| Vite | 7.x | Build tool & dev server |
| React Router DOM | 7.x | Client-side routing |
| Redux Toolkit | 2.x | Global state management |
| React Redux | 9.x | React bindings for Redux |
| @clerk/clerk-react | 5.x | Authentication (UI + token) |
| Axios | 1.x | HTTP client |
| Tailwind CSS | 4.x | Utility-first styling |
| Lucide React | 0.5x | Icon library |
| React Hot Toast | 2.x | Toast notifications |
| Moment.js | 2.x | Date/time formatting |
| Package | Version | Purpose |
|---|---|---|
| Express | 5.x | HTTP server & routing |
| Mongoose | 8.x | MongoDB ODM |
| @clerk/express | 1.x | Clerk middleware for token validation |
| Multer | 2.x | File upload handling |
| ImageKit | 6.x | Cloud image storage & CDN |
| Inngest | 3.x | Background jobs & event-driven functions |
| Nodemailer | 7.x | Email sending via SMTP |
| CORS | 2.x | Cross-Origin Resource Sharing |
| Dotenv | 17.x | Environment variable loading |
| Nodemon | 3.x | Dev server with auto-restart |
| Service | Purpose |
|---|---|
| MongoDB Atlas | Cloud database |
| Clerk | Authentication & user management |
| ImageKit | Image/video CDN & transformations |
| Inngest | Serverless background job orchestration |
| Brevo (Sendinblue) | SMTP relay for transactional emails |
| Vercel | Frontend & backend deployment |
LoopIn/
├── client/ # React frontend (Vite)
│ ├── public/
│ ├── src/
│ │ ├── api/
│ │ │ └── axios.js # Axios instance with base URL
│ │ ├── app/
│ │ │ └── store.js # Redux store configuration
│ │ ├── features/
│ │ │ ├── user/
│ │ │ │ └── userSlice.js # User state + async thunks
│ │ │ ├── connections/
│ │ │ │ └── connectionSlice.js # Connections/followers state
│ │ │ └── messages/
│ │ │ └── messagesSlice.js # Chat messages state
│ │ ├── pages/
│ │ │ ├── Login.jsx # Auth page
│ │ │ ├── Layout.jsx # Sidebar + Outlet wrapper
│ │ │ ├── Feed.jsx # Posts feed
│ │ │ ├── CreatePost.jsx # Post creation form
│ │ │ ├── Messages.jsx # Recent conversations list
│ │ │ ├── ChatBox.jsx # Real-time chat window
│ │ │ ├── Connections.jsx # Manage connections/followers
│ │ │ ├── Discover.jsx # Search users
│ │ │ └── Profile.jsx # User profile page
│ │ ├── components/
│ │ │ ├── Sidebar.jsx
│ │ │ ├── PostCard.jsx
│ │ │ ├── StoriesBar.jsx
│ │ │ ├── StoryModal.jsx
│ │ │ ├── StoryViewer.jsx
│ │ │ ├── ProfileModal.jsx
│ │ │ ├── UserCard.jsx
│ │ │ ├── UserProfileInfo.jsx
│ │ │ ├── RecentMessages.jsx
│ │ │ ├── Notification.jsx
│ │ │ ├── MenuItem.jsx
│ │ │ └── Loading.jsx
│ │ ├── App.jsx # Routes + SSE connection setup
│ │ ├── main.jsx # Root entry point
│ │ └── index.css
│ ├── vercel.json # Vercel SPA rewrite rules
│ ├── vite.config.js
│ └── package.json
│
└── server/ # Node.js + Express backend
├── configs/
│ ├── db.js # MongoDB connection via Mongoose
│ ├── imageKit.js # ImageKit SDK configuration
│ ├── multer.js # Multer disk storage setup
│ └── nodeMailer.js # Nodemailer transporter (Brevo SMTP)
├── middlewares/
│ └── auth.js # Clerk-based protect middleware
├── models/
│ ├── User.js # User schema
│ ├── Post.js # Post schema
│ ├── Connection.js # Connection request schema
│ ├── Message.js # Chat message schema
│ └── Story.js # Story schema
├── routes/
│ ├── userRoutes.js
│ ├── postRoutes.js
│ ├── storyRoutes.js
│ └── messageRoutes.js
├── controllers/
│ ├── userController.js
│ ├── postController.js
│ ├── storyController.js
│ └── messageController.js
├── inngest/
│ └── index.js # All Inngest background functions
├── server.js # Express app entry point
├── vercel.json # Vercel serverless config
└── package.json
_id String (Clerk User ID) — Primary key, not auto-generated
email String, required
full_name String, required
username String, unique
bio String, default: "Hey there! I am using LoopIn."
profile_picture String, default: ""
cover_photo String, default: ""
location String, default: ""
followers [String] — Array of User IDs
following [String] — Array of User IDs
connections [String] — Array of User IDs (mutual connections)
createdAt Date (auto)
updatedAt Date (auto)
user String (ref: User) — Author
content String
image_urls [String] — CDN URLs (max 4)
post_type Enum: text | image | text_with_image
likes_count [String] — Array of User IDs who liked
createdAt Date (auto)
updatedAt Date (auto)
from_user_id String (ref: User)
to_user_id String (ref: User)
status Enum: pending | accepted, default: pending
createdAt Date (auto)
updatedAt Date (auto)
from_user_id String (ref: User)
to_user_id String (ref: User)
text String (trimmed)
message_type Enum: text | image
media_url String
seen Boolean, default: false
createdAt Date (auto)
updatedAt Date (auto)
user String (ref: User)
content String
media_url String
media_type Enum: text | image | video
views_count [String] — Array of User IDs who viewed
background_color String
createdAt Date (auto)
updatedAt Date (auto)
All protected routes require the header:
Authorization: Bearer <clerk_jwt_token>
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /data |
✅ | Get current user's data |
| POST | /update |
✅ | Update profile (name, bio, location, photos) |
| POST | /discover |
✅ | Search users by name, email, username, location |
| POST | /follow |
✅ | Follow a user |
| POST | /unfollow |
✅ | Unfollow a user |
| POST | /connect |
✅ | Send a connection request |
| POST | /accept |
✅ | Accept a connection request |
| GET | /connections |
✅ | Get connections, followers, following, pending requests |
| POST | /profiles |
❌ | Get a user's public profile + their posts |
| GET | /recent-messages |
✅ | Get recent messages received |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /add |
✅ | Create a new post (supports up to 4 images) |
| GET | /feed |
✅ | Get feed posts from connections + following |
| POST | /like |
✅ | Toggle like/unlike on a post |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /create |
✅ | Create a new story (text/image/video) |
| GET | /get |
✅ | Get stories from connections + following |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /:userId |
❌ | Open SSE stream for real-time messages |
| POST | /send |
✅ | Send a message (text or image) |
| POST | /get |
✅ | Get chat history with a specific user |
Note: The SSE endpoint does not use the
protectmiddleware because the browser's nativeEventSourceAPI cannot send custom headers.
Inngest handles all asynchronous and scheduled tasks. The webhook endpoint is exposed at /api/inngest.
| Function | Trigger | Description |
|---|---|---|
sync-user-from-clerk |
clerk/user.created |
Creates a new User in MongoDB when someone registers via Clerk. Auto-generates a unique username from their email. |
update-user-from-clerk |
clerk/user.updated |
Syncs name, email, and profile picture from Clerk to MongoDB on profile update. |
delete-user-with-clerk |
clerk/user.deleted |
Removes the User document from MongoDB when their Clerk account is deleted. |
send-new-connection-request-reminder |
app/connection-request |
Sends an email immediately when a connection request is sent, then waits 24 hours and sends a reminder if it's still pending. |
story-delete |
app/story.delete |
Waits exactly 24 hours after a story is created using step.sleepUntil(), then deletes it from MongoDB. |
send-unseen-messages-notification |
Cron: 0 9 * * * (9 AM EST) |
Finds all unread messages daily and emails each recipient their unseen message count with a link to the app. |
- Node.js >= 18.x
- npm >= 9.x
- A MongoDB Atlas cluster
- A Clerk application (for auth)
- An ImageKit account (for image storage)
- An Inngest account (for background jobs)
- A Brevo account (for SMTP email)
# Frontend URL (for email links)
FRONTEND_URL=http://localhost:5173
# MongoDB Atlas connection string
MONGODB_URL=mongodb+srv://<username>:<password>@cluster.mongodb.net
# Inngest
INNGEST_EVENT_KEY=your_inngest_event_key
INNGEST_SIGNING_KEY=your_inngest_signing_key
# Clerk
CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# ImageKit
IMAGEKIT_PUBLIC_KEY=public_...
IMAGEKIT_PRIVATE_KEY=private_...
IMAGEKIT_URL_ENDPOINT=https://ik.imagekit.io/your_id
# Email (Brevo SMTP)
SENDER_EMAIL=your@email.com
SMTP_USER=your_brevo_smtp_user
SMTP_PASS=your_brevo_smtp_password# Your backend URL
VITE_BASE_URL=http://localhost:4000
# Clerk publishable key
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...git clone https://github.com/your-username/loopin.git
cd loopincd server
npm installcd ../client
npm installCreate server/.env and client/.env files using the templates above.
cd server
npm run server # Uses nodemon for auto-reloadThe server runs at http://localhost:4000
npx inngest-cli@latest dev -u http://localhost:4000/api/inngestThis is required to run background jobs locally. The Inngest dev server listens for events and invokes your local functions.
cd client
npm run devThe frontend runs at http://localhost:5173
Both client and server are independently deployed to Vercel.
The client/vercel.json contains a catch-all rewrite rule so React Router handles all routes on the client side:
{
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
}Set VITE_BASE_URL and VITE_CLERK_PUBLISHABLE_KEY in Vercel environment variables for the frontend project.
The server/vercel.json uses @vercel/node to deploy Express as a serverless function:
{
"version": 2,
"builds": [{ "src": "server.js", "use": "@vercel/node" }],
"routes": [{ "src": "/(.*)", "dest": "server.js" }]
}Set all server .env variables in Vercel environment variables for the backend project.
After deployment: Update
FRONTEND_URLin the backend andVITE_BASE_URLin the frontend to the deployed URLs. Also update the Clerk dashboard with your production domain and the Inngest dashboard with your production server URL.
- User signs up/in through the Clerk-hosted UI component
- Clerk fires a
clerk/user.createdwebhook to the Inngest endpoint - Inngest's
syncUserCreationfunction creates the user in MongoDB using the Clerk user ID as_id - On every API call, the frontend attaches the Clerk JWT in the
Authorizationheader clerkMiddleware()decodes the token andprotectmiddleware validates it — makingreq.auth()available in controllers
- When the app loads and a user is authenticated,
App.jsxopens a persistentEventSourceconnection to/api/message/:userId - The server stores the
resobject in aconnectionsin-memory map keyed by userId - When User B sends a message to User A via
POST /api/message/send:- The message is saved to MongoDB
- The sender receives a
200 OKimmediately - The server then pushes the message to User A's open SSE stream via
connections[userId].write(...)
- In
App.jsx,eventSource.onmessagefires — if the user is on that chat page, the message is added to Redux; otherwise a toast notification is shown
- When a story is created, the controller fires
inngest.send({ name: 'app/story.delete', data: { storyId } }) - The Inngest
deleteStoryfunction runsstep.sleepUntil(now + 24 hours)— it pauses without blocking any thread - After 24 hours, Inngest resumes and calls
Story.findByIdAndDelete(storyId)
Multermiddleware receives the file and saves it to the OS temp directory- The controller reads the file as a buffer using
fs.readFileSync(file.path) - The buffer is uploaded to ImageKit via their SDK
- ImageKit returns a
filePath; we build an optimized CDN URL with transformations (auto quality, WebP format, max width) - The CDN URL is stored in MongoDB — files are never stored on the server permanently
Built by Ajith Kumar Guntipalli