A full-stack, SaaS-style note management application where notes are more than plain text — AI automatically generates titles, summarizes long content, and lets you chat with your own notes using Retrieval-Augmented Generation (RAG), automatically detects reminders and deadlines, surfaces semantically related notes, and lets you merge notes intelligently.
Built with the MERN stack (MongoDB, Express, React, Node.js), Redux Toolkit, Google Gemini, ChromaDB,Redis and BullMQ.
Live: https://notevault-pro.vercel.app
- Features
- Tech Stack
- Architecture
- Folder Structure
- Local Setup
- Environment Variables
- External Services Required
- Deployment
- Scripts
- Known Limitations
- Create, edit, delete notes — title is completely optional
- Pin, archive, trash, and restore notes
- Flat labels (not folders) for cross-cutting organization
- Multi-select with bulk pin / archive / delete / restore
- Duplicate a note ("Make a copy")
- Copy note content to clipboard
- Keyword search with live text highlighting (multi-word, order-independent)
- Two-column grid on mobile, three-column on desktop — pinned notes always shown first
- AI Title Generation — one-click title suggestion based on note content
- AI Summarization — on-demand summary for any note, with regenerate option
- Semantic Embeddings — every note is embedded in the background (BullMQ + Redis) and stored in ChromaDB
- RAG Chat — a slide-in chat panel that answers questions across all your notes using retrieved context, with source note references
- Related Notes — opens automatically inside the note editor showing 3-4 semantically similar notes as horizontally scrollable mini-cards; clicking one instantly swaps the editor to that note — a "second brain" experience with no manual linking required
- Smart Reminder Extraction — background job automatically detects dates, deadlines, meetings, and time-sensitive tasks from note content ("Meet John on Friday", "Submit report by 5th") and surfaces them as amber bell chips on the note card
- Note Merging — select 2+ notes, click Merge → instantly concatenates into one note with
--- Section ---headings, auto-generates an AI title, and opens the editor. Optional "Organize with AI" button inside the merged note reorganizes content by topic/theme instead of by original note — preserving every fact, just structured better - Organize with AI — available on any merged note; Gemini intelligently reorganizes the content without losing any information; gracefully falls back on Gemini 503 errors with the original content unchanged
- JWT authentication with access token (in-memory) + rotating refresh token (httpOnly cookie)
- Email/password registration and login with strict validation
- Google OAuth 2.0 sign-in/sign-up (via Passport.js)
- Account linking — same email via Google or password logs into the same account
- Set Password flow for Google-only users who want email login too
- Change Password flow for existing password users
- Forgot / Reset Password via email (Resend)
- Editable profile (display name, avatar color)
- Rate-limited auth endpoints (brute-force protection)
- Extracted automatically in the background after every note save — no user action needed
- Amber bell chips appear on note cards with reminder text and datetime
- Clicking a chip opens a popover with full actions:
- Got it — marks as acknowledged (chip goes strikethrough/muted)
- Undo — reverts an accidental acknowledgment back to active
- Remove — permanently deletes the reminder from the note
- Duplicate-detection prevents the same reminder being extracted twice on subsequent saves
- Select 2 or more notes using multi-select checkboxes
- Click Merge in the selection navbar — notes concatenated instantly, AI title auto-generated, editor opens
- Organize with AI button (shown only on merged notes) — Gemini restructures content by theme, removes
--- Section ---markers, de-duplicates overlapping points - Retry logic handles Gemini 503 (overload) errors — up to 2 retries with exponential backoff before graceful failure
- On failure, original merged content is preserved exactly — nothing is ever lost
- Professional design system: custom color tokens, Sora (display) + Inter (body) + JetBrains Mono (AI metadata) typography
- Full light/dark mode
- Collapsible desktop sidebar + mobile drawer navigation
- Toast notifications (Sonner) for every action
- Context-aware empty states for every view (All / Pinned / Archive / Trash / Label / Search)
- Confirmation dialogs for destructive actions (permanent delete)
- Error boundary to prevent full-app crashes
- 404 page with navigation back to notes
- Fully responsive (mobile, tablet, desktop)
| Layer | Technology |
|---|---|
| Frontend | React (Vite), Redux Toolkit + RTK Query, React Router, Tailwind CSS, shadcn/ui, react-hook-form + Zod, Sonner |
| Backend | Node.js, Express |
| Database | MongoDB (Mongoose) |
| Auth | JWT (access + refresh), Passport.js (Google OAuth 2.0), bcrypt |
| AI (generation + embeddings) | Google Gemini API (gemini-3.6-flash for generation, gemini-embedding-001 for embeddings) |
| Vector Database | ChromaDB (local via Docker in dev, Chroma Cloud in production) |
| Background Jobs | BullMQ + Redis (local via Docker in dev, Upstash Redis in production) |
| Transactional Email | Resend |
| Deployment | Vercel (frontend), Render (backend) |
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ React │◄──────►│ Express │◄──────►│ MongoDB │
│ Redux Toolkit│ │ API Layer │ │ Atlas │
│ (Vercel) │ │ (Render) │ │ │
└──────────────┘ └──────┬───────┘ └──────────────┘
▲ │
│ /api/* proxy │
│ (same-origin cookie) │
└───────────────────────┘
│
┌───────────┴────────────┐
│ BullMQ + Upstash Redis │
│ Job types: │
│ • embed-note │
│ • extract-reminders │
└───────────┬────────────┘
│
┌───────────┴────────────┐
│ Google Gemini API │
│ • Text generation │
│ • Embeddings │
└───────────┬────────────┘
│
┌───────────┴────────────┐
│ ChromaDB (Chroma Cloud)│
│ • Semantic memory │
│ • RAG retrieval │
│ • Related notes │
└────────────────────────┘
Request flow for AI features:
- Title / Summary / Organize — synchronous → Gemini → response (user is actively waiting, 1-2s acceptable)
- Embeddings + Reminder extraction — BullMQ job queued on every note save; background worker processes silently; user never waits
- RAG Chat — the question is embedded, ChromaDB returns the most relevant notes, their content is passed as context to Gemini, which generates a grounded answer with source references.
- Related Notes — reuses stored note embedding from ChromaDB (no extra Gemini call) → nearest neighbors queried → shown in editor
- Cookie security — refresh token is httpOnly + Secure + SameSite=Lax; all API calls proxied through Vercel so cookie is always first-party regardless of browser third-party cookie blocking
notes-ai/
├── backend/
│ ├── src/
│ │ ├── config/ # db, redis, chroma, gemini, passport
│ │ ├── models/ # User, Note (Mongoose schemas)
│ │ ├── controllers/ # auth, note, ai controllers
│ │ ├── routes/ # auth, note, ai routes
│ │ ├── middleware/ # auth (protect), error handling
│ │ ├── queues/ # BullMQ queue (embed-note + extract-reminders) + worker
│ │ ├── utils/ # token generation, embeddings, email
│ │ └── app.js # Express app (middleware, routes)
│ ├── server.js # Entry point (loads env, connects DB/Redis, starts server + worker)
│ └── package.json
│
├── frontend/
│ ├── src/
│ │ ├── app/ # Redux store
│ │ ├── features/ # auth, notes, theme, selection, ai (RTK Query slices)
│ │ ├── services/ # base RTK Query API (with token refresh interceptor)
│ │ ├── components/ # layout, notes, chat, ui (shadcn)
│ │ ├── pages/ # Login, Register, Dashboard, Settings, etc.
│ │ ├── routes/ # ProtectedRoute
│ │ ├── lib/ # validation schemas (Zod), utils (formatRetryTime, highlightText)
│ │ └── App.jsx
│ ├── vercel.json # SPA rewrites + API proxy to backend
│ └── package.json
│
└── README.md
- Node.js 18+
- Docker Desktop (for local Redis + ChromaDB)
- A MongoDB Atlas account (free tier)
- A Google Gemini API key (free tier)
git clone https://github.com/YOUR_USERNAME/notes-ai.git
cd notes-ai
cd backend && npm install
cd ../frontend && npm installdocker run -d --name redis -p 6379:6379 redis:alpine
docker run -d --name chromadb -p 8000:8000 -v chroma-data:/chroma/chroma chromadb/chroma:latestCopy the example files and fill in your own values (see Environment Variables below):
cd backend
cp .env.example .envcd frontend
cp .env.example .env# Terminal 1 — backend
cd backend
npm run dev
# Terminal 2 — frontend
cd frontend
npm run dev- Backend:
http://localhost:5000 - Frontend:
http://localhost:5173
# Server
PORT=5000
NODE_ENV=development
CLIENT_URL=http://localhost:5173
# MongoDB
MONGO_URI=mongodb+srv://<user>:<password>@cluster0.mongodb.net/notes-ai-saas
# JWT
JWT_ACCESS_SECRET=long_random_string
JWT_REFRESH_SECRET=different_long_random_string
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d
# Google Gemini
GEMINI_API_KEY=your_gemini_api_key
GEMINI_EMBEDDING_MODEL=gemini-embedding-001
# Redis (local Docker in dev, Upstash in prod)
REDIS_URL=redis://localhost:6379
# ChromaDB — local dev
CHROMA_URL=http://localhost:8000
CHROMA_COLLECTION=notes
# ChromaDB — production (Chroma Cloud)
CHROMA_HOST=api.trychroma.com
CHROMA_TENANT=your-tenant-id
CHROMA_DATABASE=your-database-name
CHROMA_API_KEY=your-chroma-cloud-api-key
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=http://localhost:5000/api/auth/google/callback
# Resend (transactional email)
RESEND_API_KEY=your_resend_api_key
RESEND_FROM_EMAIL=NoteVault <onboarding@resend.dev>
RESET_PASSWORD_URL=http://localhost:5173/reset-passwordVITE_API_URL=http://localhost:5000/apiIn production, this is set to a relative
/apipath and proxied through Vercel to the Render backend — see Deployment.
All services below have a free tier sufficient for development and a small-scale production deployment.
| Service | Purpose | Sign up |
|---|---|---|
| MongoDB Atlas | Primary database | https://www.mongodb.com/cloud/atlas/register |
| Google AI Studio | Gemini API key (generation + embeddings) | https://aistudio.google.com/app/apikey |
| Google Cloud Console | OAuth 2.0 credentials | https://console.cloud.google.com |
| Chroma Cloud | Managed vector database (production) | https://trychroma.com |
| Upstash | Managed Redis (production) | https://upstash.com |
| Resend | Transactional email (password reset) | https://resend.com |
| Render | Backend hosting | https://render.com |
| Vercel | Frontend hosting | https://vercel.com |
Frontend → Vercel
Backend → Render
Database → MongoDB Atlas (M0 free tier)
Vectors → Chroma Cloud
Queue → Upstash Redis
Email → Resend
AI → Google Gemini API
- Push code to GitHub.
- Create a Web Service on Render, connect the repo.
- Set Root Directory to
backend. - Build command:
npm install· Start command:node server.js. - Add all backend environment variables (production values — see above).
- Set
app.set("trust proxy", 1)is already configured to work correctly behind Render's reverse proxy. - In MongoDB Atlas → Network Access → allow
0.0.0.0/0so Render can connect.
- Import the repo into Vercel, set Root Directory to
frontend. - Framework preset: Vite. Build command:
npm run build. Output:dist. - Set
VITE_API_URL=/api(relative — see proxy note below). - Add
frontend/vercel.jsonwith API proxy + SPA rewrites:
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://YOUR-RENDER-URL.onrender.com/api/:path*"
},
{ "source": "/(.*)", "destination": "/index.html" }
]
}Why the proxy is required: the frontend and backend are on different domains. Routing
/api/*through Vercel makes the refresh-token cookie first-party instead of third-party — this is what allows the cookie to be set reliably even with browsers that block third-party cookies by default. Cookies are configured ashttpOnly,secure,SameSite=Lax.
Add your production callback URL to Google Cloud Console → Credentials → Authorized redirect URIs:
https://YOUR-RENDER-URL.onrender.com/api/auth/google/callback
Update GOOGLE_CALLBACK_URL and CLIENT_URL on Render accordingly.
Create a database + collection on Chroma Cloud, then set CHROMA_HOST, CHROMA_TENANT, CHROMA_DATABASE, CHROMA_API_KEY on Render. The client automatically switches between local Docker ChromaDB (dev) and Chroma Cloud (NODE_ENV=production) — see backend/src/config/chroma.js.
Create a Redis database on Upstash, copy the rediss:// connection string (note the double s — TLS required) into REDIS_URL on Render.
| Command | Location | Description |
|---|---|---|
npm run dev |
backend | Start Express server with nodemon + embedding worker |
npm run dev |
frontend | Start Vite dev server |
npm run build |
frontend | Production build |
| Method | Route | Description |
|---|---|---|
| POST | /api/auth/register |
Register with email/password |
| POST | /api/auth/login |
Login with email/password |
| POST | /api/auth/refresh |
Refresh access token via cookie |
| POST | /api/auth/logout |
Logout, invalidate refresh token |
| GET | /api/auth/me |
Get current user |
| GET | /api/auth/google |
Initiate Google OAuth |
| GET | /api/auth/google/callback |
Google OAuth callback |
| PATCH | /api/auth/set-password |
Set password for Google-only account |
| PATCH | /api/auth/change-password |
Change existing password |
| PATCH | /api/auth/update-profile |
Update name / avatar color |
| POST | /api/auth/forgot-password |
Send password reset email |
| POST | /api/auth/reset-password |
Reset password with token |
| GET | /api/notes |
Get all notes (with filters) |
| POST | /api/notes |
Create note |
| PUT | /api/notes/:id |
Update note |
| DELETE | /api/notes/:id |
Trash note |
| POST | /api/notes/merge |
Merge multiple notes into one |
| GET | /api/notes/:id/related |
Get semantically related notes |
| PATCH | /api/notes/:id/pin |
Toggle pin |
| PATCH | /api/notes/:id/archive |
Toggle archive |
| PATCH | /api/notes/:id/restore |
Restore from trash |
| DELETE | /api/notes/:id/permanent |
Delete forever |
| POST | /api/notes/:id/copy |
Duplicate note |
| PATCH | /api/notes/:id/reminders/:rid/acknowledge |
Acknowledge reminder |
| PATCH | /api/notes/:id/reminders/:rid/unacknowledge |
Undo acknowledgment |
| DELETE | /api/notes/:id/reminders/:rid |
Remove reminder permanently |
| POST | /api/ai/generate-title |
Generate title with Gemini |
| POST | /api/ai/summarize |
Summarize note with Gemini |
| POST | /api/ai/organize |
Organize merged note content with Gemini |
| POST | /api/ai/chat |
RAG chat — answer questions from notes |
- Free-tier latency — Render, Atlas M0, Upstash, and Chroma Cloud are shared infrastructure. Expect 400ms–1.5s per request (cross-region hops) and up to 15s cold-start if the Render instance was idle. Use a keep-alive ping service (cron-job.org) to mitigate cold starts.
- Email validation — format and domain-length checks only; no email verification step. A real email is not enforced at the code level.
- Reminder datetime parsing — Gemini returns the datetime string exactly as written in the note ("Friday", "by 5th") — no normalization to actual Date objects, so no calendar integration or push notifications yet.
- Google OAuth on free tier Gemini — model availability varies by Google Cloud project. If
gemini-3.6-flashbecomes unavailable, updateGEMINI_EMBEDDING_MODELand the default ingemini.js. A model-discovery script is included in the repo. - Semantic search removed from main search — the search bar uses client-side keyword matching (fast, predictable, Keep-style). Semantic/vector search is used only in RAG chat and Related Notes where it provides genuine value over keyword matching.
Meet Vora
- Email: meetvora877@gmail.com
- LinkedIn: https://linkedin.com/in/meetvora79