Gmail is chaos. Job alerts buried under food delivery notifications. Internship emails lost in promotional spam. Razorpay transactions, Swiggy receipts, Naukri pings β all flattened into one endless list.
InboxIt connects to your Gmail, syncs emails in the background using a chunked BullMQ queue engine, and automatically drops them into smart folders: Jobs, Finance, Food, Travel, Health, Social. No manual filters. No sorting rules. Just open the app and your inbox is already organized.
Built entirely from scratch β no email SDK shortcuts, no pre-built inbox templates. Raw Gmail API, custom MIME parser, custom classifier, production-grade sync architecture. Deployed and live at inboxit.vercel.app.
| Feature | What it does |
|---|---|
| π Google OAuth2 | One-click sign-in, JWT issued on login, stored in cookies |
| β‘ Chunked Sync Engine | Syncs 500 emails/chunk via BullMQ, fully resumable |
| π Live Tracker | Incremental sync every 60s via Gmail History API |
| ποΈ Smart Folders | Auto-classifies into Jobs, Finance, Food, Travel, Health, Social |
| β‘ Redis Caching | Folder queries served in <50ms after first load |
| π Load More | Lazy-load older emails on demand |
| ποΈ Trash / Archive | Syncs back to real Gmail β delete in InboxIt, gone in Gmail |
| π Token Auto-Refresh | Gmail OAuth token refreshed silently before every request |
| π‘ WebSockets | Real-time sync progress pushed to frontend via socket.io |
| π Search | Search across your synced emails |
| π REST API | Clean endpoints for list, detail, update, delete, archive |
πΈ Screenshots Dashboard β Smart Folders in action
Inbox View β Live sync progress
--- ## ποΈ Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client (Next.js Β· Vercel) β
β JWT in js-cookie Β· SWR for data Β· socket.io-client v4 β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β REST (JWT) β socket.io (real-time)
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β Express 5 API Server β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β GET /emails β β POST /sync β β PATCH/DELETE /emails β β
β β Redis cache β β BullMQ enqueueβ β MongoDB + cache bust β β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ ββββββββββββββββββββββββ β
β β β β
β ββββββββΌβββββββ ββββββββΌβββββββββββββββββββββββββββββββββββ β
β β MongoDB β β BullMQ Worker β β
β β Atlas β β 1. Fetch 500 emails from Gmail API β β
β β (emails + β β 2. Parse MIME (custom parser) β β
β β users) β β 3. Classify β smart folder β β
β βββββββββββββββ β 4. Upsert into MongoDB (idempotent) β β
β β 5. Bust Redis cache β β
β βββββββββββββββ β 6. Save nextPageToken (resumable) β β
β β Redis β β 7. Emit progress via socket.io β β
β β (Upstash) β β 8. Start 60s incremental tracker β β
β β BullMQ qs β ββββββββββββββββββββββββββββββββββββββββββ-β β
β β Folder cacheβ β
β βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The sync engine works in two distinct phases:
Phase 1 β Lazy Load (on first login)
- POST
/syncdrops a job into BullMQ and returns200 OKimmediately - Worker downloads the 500 most recent emails from
INBOX - Parses, classifies, and upserts into MongoDB
- Saves
nextPageTokenfor the "Load More" button - Stops β doesn't dump your entire email history
Phase 2 β Live Track (every 60 seconds after)
- Worker enqueues a periodic
incrementalsync job - Uses Gmail's
history.listAPI withlastHistoryIdas the cursor - Downloads only new/changed emails since last check
0 new emails= valid success state, not a panic trigger- Pushes progress events to the client via socket.io
The classifier runs on every email using sender domain + subject keyword matching:
| Folder | What goes here |
|---|---|
| πΌ jobs | Naukri, LinkedIn, Internshala, Wellfound, Cutshort, Instahyre, Hirist, Accenture, Freshers, Careers |
| π° finance | Banks, HDFC, SBI, Paytm, PhonePe, Razorpay, GPay, ICICI, mutual funds, statements |
| π food | Swiggy, Zomato, Zepto, food delivery receipts |
| π travel | Uber, Ola, Rapido, ride receipts, travel bookings |
| π₯ health | Apollo, Practo, PharmEasy, 1MG, doctor, appointment |
| π₯ social | Facebook, Instagram, Twitter, WhatsApp, Reddit, LinkedIn social |
| π₯ inbox | Everything else, not archived, sorted by date |
User Schema
{
googleId, email, name, avatar,
accessToken, refreshToken, tokenExpiry,
lastHistoryId, // cursor for incremental sync
lastSyncedAt,
nextPageToken, // cursor for load-more
totalSynced,
isSyncing, // distributed lock
syncStartedAt // auto-unlock after 10 min if stuck
}Email Schema
{
userId, // ref to User
gmailMessageId, // unique per user (compound index)
threadId,
from, to, subject, snippet,
bodyHtml, bodyText,
labels, // native Gmail labels [INBOX, SENT, ...]
categories, // custom: ["jobs", "finance", ...]
receivedAt,
isRead, isStarred, isDeleted
}| Method | Endpoint | Description |
|---|---|---|
GET |
/auth/google |
Redirect to Google OAuth |
GET |
/auth/google/callback |
OAuth callback, issues JWT |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/sync |
Trigger full or incremental sync |
POST |
/api/sync/load-more |
Resume next 500-email chunk |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/emails?folder=jobs&page=1 |
List emails by smart folder |
GET |
/api/emails/:id |
Full email with bodyHtml + bodyText |
PATCH |
/api/emails/:id |
Update isRead, isStarred, category |
DELETE |
/api/emails/:id |
Soft delete + Gmail Trash |
POST |
/api/emails/:id/archive |
Gmail Archive + remove from inbox |
INBOXIT/
βββ server/
β βββ src/
β β βββ config/
β β β βββ db.js # MongoDB Atlas connection
β β β βββ redis.js # Upstash node-redis v5 + keepAlive
β β β βββ passport.js # Google OAuth2 strategy
β β β βββ socket.js # socket.io init + getIO() helper
β β βββ controllers/
β β β βββ emailController.js # cache-aside, CRUD logic
β β β βββ syncController.js
β β βββ middleware/
β β β βββ authMiddleware.js # JWT verify
β β β βββ tokenRefreshMiddleware.js # silent token refresh
β β βββ models/
β β β βββ User.js
β β β βββ Email.js
β β βββ queues/
β β β βββ syncQueue.js # BullMQ Queue definition
β β β βββ syncWorker.js # The core sync engine
β β βββ routes/
β β β βββ authRoutes.js
β β β βββ syncRoutes.js
β β β βββ emailRoutes.js
β β βββ services/
β β β βββ classifier.js # Rule-based email classifier
β β βββ utils/
β β βββ gmailClient.js # Authenticated Gmail API instance
β β βββ mimeParser.js # Raw MIME β bodyHtml/bodyText
β β βββ jwt.js
β βββ index.js
βββ client/ # Next.js 16 + React 19 β deployed on Vercel
β βββ app/
β β βββ auth/success/ # OAuth callback handler
β β βββ dashboard/
β β β βββ inbox/ # All mail view
β β β βββ [folder]/ # Dynamic smart folder route
β β βββ layout.tsx
β β βββ page.tsx # Landing page
β βββ components/
β β βββ Sidebar.tsx # Smart folder navigation
β β βββ EmailList.tsx # Paginated email list
β β βββ EmailDetail.tsx # Email body (DOMPurify sanitized)
β β βββ SearchBar.tsx # Client-side search
β β βββ SyncProgressBar.tsx # Real-time sync progress via socket.io
β β βββ Toast.tsx # Notifications
β βββ lib/ # API helpers + socket client
β βββ middleware.ts # JWT auth guard on protected routes
βββ docker-compose.yml
Every single one of these hit in real development and got fixed.
Bug 1 β BullMQ worker silently never started
Root causes (4 separate issues):
maxRetriesPerRequest: nullmissing β BullMQ v5 requires this or the worker silently refuses to starttls: {}vstls: { rejectUnauthorized: false }β Upstash requires the latter- Stale failed jobs with a fixed
jobIdpermanently blocked all future jobs β fixed withremoveOnFail: { count: 3 } - Worker file not imported in
index.js
Bug 2 β isSyncing deadlock (stuck forever)
Redis dropped mid-job β BullMQ marked job as stalled β lock never released β every subsequent sync silently skipped.
Fix: Three escape hatches:
stall/failedevent handlers reset the flag immediatelysyncStartedAttimestamp β auto-unlocks after 10 minutes- Manual reset via MongoDB update for local recovery
Bug 3 β Job stalling on large syncs (duplicates)
Sequential processing of large batches exceeded BullMQ's default lock duration β job marked as stalled β retried β same emails inserted again.
Fix: lockDuration: 10 * 60 * 1000 + job.updateProgress() every 500 emails to heartbeat the lock.
Bug 4 β Redis connection reset on long sync jobs
Upstash closes idle connections. A long sync job goes stretches without touching Redis. Connection dies silently.
Fix: keepAlive + retryStrategy + reconnectOnError in node-redis config.
Bug 5 β The "Traffic Jam" (Redis quota blowout)
Express route was firing the initial chunk AND starting the 60-second periodic timer simultaneously. The timer kept waking up, seeing pending pages, and spamming the queue.
Fix: Strict handoff β 60-second timer only starts after the initial chunk is 100% complete.
Bug 6 β Duplicate emails in MongoDB
Email.create() blindly inserted every time. Retried jobs = duplicate documents.
Fix: findOneAndUpdate({ userId, gmailMessageId }, { $set: data }, { upsert: true }) + compound unique index { userId, gmailMessageId }.
Bug 7 β Emails appearing in wrong order
Worker processes 100 emails concurrently via Promise.all. Whichever API response resolves first saves to MongoDB first. Default MongoDB sort is by _id (insertion time), not email date.
Fix: No worker changes needed. emailController.js always applies .sort({ receivedAt: -1 }) when serving to the frontend.
Bug 8 β Gmail client failing mid-request
getGmailClient(req.user.id) was passed just the ID string, but the function needs the full user object with accessToken and refreshToken.
Fix: Always fetch full user first β const user = await User.findById(req.user.id) β then pass user to getGmailClient.
| Decision | Why |
|---|---|
| JWT over sessions | Stateless β works cleanly with Next.js frontend, no server-side session store |
| JWT in js-cookie | Accessible from Next.js client, simpler than httpOnly for this architecture |
| BullMQ over in-process async | Sync jobs survive server restarts, fully observable, retryable |
| Upsert everywhere | Idempotency is non-negotiable in any sync system |
Compound index {userId, gmailMessageId} |
gmailMessageId alone isn't globally unique β different users can have the same ID |
labelIds: ["INBOX"] |
Without this, messages.list returns Sent, Trash, Spam β everything |
| Cache-aside, not write-through | Simpler to reason about, bust on every write, TTL handles the rest |
| Separate Redis connections for Queue and Worker | BullMQ requires this β sharing one connection causes silent failures |
| socket.io over raw WebSockets | Built-in reconnection, rooms (one per userId), fallback to long-polling |
| DOMPurify on email body | Email HTML can contain scripts β always sanitize before rendering |
- Node.js 18+
- MongoDB Atlas URI
- Redis (Upstash β free tier works)
- Google Cloud project with Gmail API + OAuth2 credentials
# Clone
git clone https://github.com/Vansh1811/INBOXIT.git
cd INBOXIT
# Server
cd server
npm install
cp .env.example .env # fill in your credentials
npm run dev
# Client (separate terminal)
cd client
npm install
npm run dev# Server (.env)
PORT=5000
MONGO_URI=mongodb+srv://...
REDIS_URL=rediss://...
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_CALLBACK_URL=http://localhost:5000/auth/google/callback
JWT_SECRET=...
FRONTEND_URL=http://localhost:3000
# Client (.env.local)
NEXT_PUBLIC_API_URL=http://localhost:5000docker-compose up --build- AI digest bar β GPT-4o-mini one-line summary per smart folder
- Compose / Reply β Gmail API
messages.sendwith thread context - Custom domain β
inboxit.in
Not a list of buzzwords. These are things that actually broke and forced me to learn.
- Idempotency is non-negotiable in any sync system β always upsert, never blindly insert
- BullMQ v5 requires
maxRetriesPerRequest: nullβ missing it = worker starts but silently never processes jobs - Queue and Worker need separate Redis connections β one shared connection causes silent, unfixable failures
tls: { rejectUnauthorized: false }is different fromtls: {}β Upstash needs the former- Stale failed jobs with a fixed
jobIdpermanently block all future jobs β useremoveOnFail - Sync locks need multiple escape hatches β event handlers + timestamp auto-unlock + manual reset
messages.listreturns everything without a label filter β always passlabelIds: ["INBOX"]- Redis closes idle connections on long jobs β always configure
keepAliveandretryStrategy nextPageTokenis your resume cursor β save it after every chunk, not at the end of the whole sync0 new emailsis a valid success state, not a panic trigger- MongoDB sorts by
_id(insertion time) by default β always sort byreceivedAtexplicitly - Always sanitize email HTML before rendering β email bodies can contain malicious scripts
Built with too much coffee and too many 3am Redis errors.
β Star this repo if you find the architecture interesting.