Skip to content

Vansh1811/INBOXIT

Repository files navigation

πŸ“¬ InboxIt

A production-grade Gmail client that auto-sorts your inbox into smart folders. Live and open source.

Live Demo Node.js Next.js MongoDB Redis BullMQ socket.io


πŸ€” Why I built this

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.


✨ Features

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 InboxIt Dashboard - Jobs Folder Inbox View β€” Live sync progress InboxIt Inbox - Live Sync --- ## πŸ—οΈ 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 "Lazy Load + Live Track" Model

The sync engine works in two distinct phases:

Phase 1 β€” Lazy Load (on first login)

  • POST /sync drops a job into BullMQ and returns 200 OK immediately
  • Worker downloads the 500 most recent emails from INBOX
  • Parses, classifies, and upserts into MongoDB
  • Saves nextPageToken for the "Load More" button
  • Stops β€” doesn't dump your entire email history

Phase 2 β€” Live Track (every 60 seconds after)

  • Worker enqueues a periodic incremental sync job
  • Uses Gmail's history.list API with lastHistoryId as 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

πŸ—‚οΈ Smart Folder Classification

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

πŸ—ƒοΈ Data Models

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
}

βš™οΈ API Reference

Auth

Method Endpoint Description
GET /auth/google Redirect to Google OAuth
GET /auth/google/callback OAuth callback, issues JWT

Sync

Method Endpoint Description
POST /api/sync Trigger full or incremental sync
POST /api/sync/load-more Resume next 500-email chunk

Emails

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

🧱 Project Structure

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

πŸ› Hard Bugs Crushed

Every single one of these hit in real development and got fixed.

Bug 1 β€” BullMQ worker silently never started

Root causes (4 separate issues):

  1. maxRetriesPerRequest: null missing β€” BullMQ v5 requires this or the worker silently refuses to start
  2. tls: {} vs tls: { rejectUnauthorized: false } β€” Upstash requires the latter
  3. Stale failed jobs with a fixed jobId permanently blocked all future jobs β€” fixed with removeOnFail: { count: 3 }
  4. 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 / failed event handlers reset the flag immediately
  • syncStartedAt timestamp β€” 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.


πŸ’‘ Key Engineering Decisions

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

πŸš€ Running Locally

Prerequisites

  • Node.js 18+
  • MongoDB Atlas URI
  • Redis (Upstash β€” free tier works)
  • Google Cloud project with Gmail API + OAuth2 credentials

Setup

# 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

Environment Variables

# 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:5000

Docker

docker-compose up --build

πŸ—ΊοΈ What's left to build

  • AI digest bar β€” GPT-4o-mini one-line summary per smart folder
  • Compose / Reply β€” Gmail API messages.send with thread context
  • Custom domain β€” inboxit.in

🧠 What I Learned Building This

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 from tls: {} β€” Upstash needs the former
  • Stale failed jobs with a fixed jobId permanently block all future jobs β€” use removeOnFail
  • Sync locks need multiple escape hatches β€” event handlers + timestamp auto-unlock + manual reset
  • messages.list returns everything without a label filter β€” always pass labelIds: ["INBOX"]
  • Redis closes idle connections on long jobs β€” always configure keepAlive and retryStrategy
  • nextPageToken is your resume cursor β€” save it after every chunk, not at the end of the whole sync
  • 0 new emails is a valid success state, not a panic trigger
  • MongoDB sorts by _id (insertion time) by default β€” always sort by receivedAt explicitly
  • 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.

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors