A production-grade notes application built with the MERN stack — containerized with Docker, served behind Nginx with SSL, and deployed on a DigitalOcean droplet.
Create, organize, and manage your personal notes with a clean, responsive interface. Fully authenticated with JWT, rate-limited via Upstash Redis, and ready for real-world use.
Live at: mernote.sandaliranahewa.dev
- User Authentication — Register, login, password reset via email (Resend), JWT-based sessions
- Full CRUD Notes — Create, read, update, and delete notes with ease
- Real-time Search — Find notes instantly as you type
- Modern UI — TailwindCSS + DaisyUI with dark/light themes (
forest&lemonade) - Rate Limiting — API protection using Upstash Redis + sliding window algorithm
- Fully Responsive — Works beautifully on desktop, tablet, and mobile
- Profile Management — Update username, email, password, and avatar
- Dockerized — Multi-stage Docker build for small production images
- Production-ready — Nginx reverse proxy + Let's Encrypt SSL on DigitalOcean
| Technology | Purpose |
|---|---|
| React 19 | UI library |
| Vite 8 | Build tool & dev server |
| TailwindCSS 3 | Utility-first CSS |
| DaisyUI 4 | TailwindCSS component library |
| React Router 7 | Client-side routing |
| Axios | HTTP client for API calls |
| Lucide React | Icon set |
| react-hot-toast | Toast notifications |
| Technology | Purpose |
|---|---|
| Node.js 20 | JavaScript runtime |
| Express 4 | Web framework |
| MongoDB / Mongoose 8 | NoSQL database & ODM |
| JWT | Authentication tokens |
| bcrypt | Password hashing |
| Upstash Redis + Ratelimit | Serverless rate limiting |
| Resend | Transactional emails (password reset) |
| Nodemon | Dev auto-restart |
| Technology | Purpose |
|---|---|
| Docker | Containerization |
| Docker Compose | Multi-service orchestration |
| Nginx | Reverse proxy (production) |
| Let's Encrypt / Certbot | Free SSL certificates |
| DigitalOcean | Cloud hosting |
| name.com | Domain & DNS management |
flowchart LR
A["User Browser"] -->|"HTTPS (443)"| B["Nginx Reverse Proxy"]
B -->|"HTTP (5001)"| C["Docker Container<br/>(mern-app)"]
C --> D["Express API Server"]
D --> E["MongoDB Atlas"]
D --> F["Upstash Redis"]
G["name.com DNS"] -->|"A Record"| H["DigitalOcean Droplet"]
H --> B
subgraph "DigitalOcean Droplet"
B
C
end
style A fill:#4a9eff,stroke:#333,color:#fff
style B fill:#009639,stroke:#333,color:#fff
style C fill:#2496ed,stroke:#333,color:#fff
style D fill:#68a063,stroke:#333,color:#fff
style E fill:#4DB33D,stroke:#333,color:#fff
style F fill:#000000,stroke:#333,color:#fff
style G fill:#ff6600,stroke:#333,color:#fff
style H fill:#0060ff,stroke:#333,color:#fff
Request flow:
- User visits
https://mernote.sandaliranahewa.dev - DNS (name.com) resolves to the DigitalOcean droplet IP
- Nginx terminates SSL and proxies requests to the Docker container on port 5001
- Express serves the React SPA (built static files) or handles API routes (
/api/*) - API server reads/writes to MongoDB Atlas (notes, users) and Upstash Redis (rate limiting)
- Node.js v18+ (for local development)
- npm or yarn
- MongoDB Atlas cluster (free tier works)
- Upstash Redis database (free tier works)
- Docker (for containerized deployment)
- Domain (for production — name.com or any registrar)
git clone https://github.com/sandaliz/MerNote.git
cd MerNote
# Install all dependencies (root + frontend + backend)
npm install
npm install --prefix frontend
npm install --prefix backendCopy the example env file and fill in your values:
cp .env.example .env.env (root):
# MongoDB (required)
MONGO_URI=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/mern-1st?retryWrites=true&w=majority
# JWT Secret (required — generate with: openssl rand -base64 32)
JWT_SECRET=your-very-long-random-secret-here
# Upstash Redis (required for rate limiting)
UPSTASH_REDIS_REST_URL=https://<your-id>.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-upstash-token
# Port (optional, defaults to 5001)
PORT=5001
# Resend API Key (required for password reset emails)
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxNever commit
.envto Git. It's already in.gitignore.
# Start both frontend & backend concurrently (recommended)
npm run devThis uses concurrently to run:
- Backend →
http://localhost:5001(with nodemon hot-reload) - Frontend →
http://localhost:5173(with Vite HMR, proxies/apito backend)
Or run them in separate terminals:
# Terminal 1 — Backend
cd backend && npm run dev
# Terminal 2 — Frontend
cd frontend && npm run dev# Build the Docker image
docker compose build
# Start the container in detached mode
docker compose up -d
# View logs
docker compose logs -f
# Visit: http://localhost:5001The multi-stage Dockerfile:
- Stage 1 — Builds the React frontend with Vite
- Stage 2 — Installs production backend dependencies
- Final stage — Copies both into a minimal
node:20-alpineimage
To stop:
docker compose downThe app is live at https://mernote.sandaliranahewa.dev — deployed with Docker on a DigitalOcean droplet with Nginx as a reverse proxy and Let's Encrypt SSL.
- Create a DigitalOcean Droplet (Ubuntu 22.04/24.04)
- Install Docker on the droplet
- Clone the repo and create a
.envwith production values - Run with Docker Compose:
docker compose up -d - Install Nginx as a reverse proxy
- Set up SSL with Certbot / Let's Encrypt
- Configure DNS (A record → droplet IP)
- Verify at
https://mernote.sandaliranahewa.dev
The app is served over HTTPS only — HTTP redirects automatically.
MerNote/
├── backend/
│ ├── src/
│ │ ├── config/
│ │ │ └── db.js # MongoDB connection
│ │ ├── controllers/
│ │ │ ├── authController.js # Auth logic (register, login, forgot/reset password)
│ │ │ ├── notesController.js # CRUD operations for notes
│ │ │ └── profileController.js # Profile management
│ │ ├── middleware/
│ │ │ ├── authMiddleware.js # JWT verification
│ │ │ ├── rateLimiter.js # Upstash Redis rate limiting
│ │ │ └── validate.js # Request validation
│ │ ├── models/
│ │ │ ├── userModel.js # User schema
│ │ │ └── noteModel.js # Note schema
│ │ ├── routes/
│ │ │ ├── authRoutes.js # /api/auth/*
│ │ │ ├── notesRoutes.js # /api/notes/*
│ │ │ └── profileRoutes.js # /api/profile/*
│ │ └── server.js # Express entry point
│ └── package.json
│
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── AuthForm.jsx # Reusable form wrapper
│ │ │ ├── AuthHeader.jsx # Auth page header
│ │ │ ├── Footer.jsx # App footer
│ │ │ ├── Navbar.jsx # Navigation bar
│ │ │ ├── NoteCard.jsx # Single note card
│ │ │ ├── NotesNotFound.jsx # Empty state
│ │ │ └── RateLimitedUI.jsx # Rate limit error UI
│ │ ├── lib/
│ │ │ ├── axios.js # Axios instance with interceptors
│ │ │ ├── colorConfig.js # Theme helpers
│ │ │ └── utils.js # Utility functions
│ │ ├── pages/
│ │ │ ├── LandingPage.jsx # Public landing page
│ │ │ ├── LoginPage.jsx # Login
│ │ │ ├── SignupPage.jsx # Signup
│ │ │ ├── HomePage.jsx # Notes dashboard
│ │ │ ├── CreatePage.jsx # Create note
│ │ │ ├── NoteDetailPage.jsx # Note detail/edit
│ │ │ ├── ProfilePage.jsx # Profile settings
│ │ │ ├── ForgotPasswordPage.jsx # Forgot password
│ │ │ └── ResetPasswordPage.jsx # Reset password
│ │ ├── App.jsx # Root component with routes
│ │ ├── main.jsx # Vite entry point
│ │ └── index.css # TailwindCSS imports
│ ├── index.html
│ ├── vite.config.js # Vite config + API proxy
│ └── tailwind.config.js # TailwindCSS + DaisyUI themes
│
├── Dockerfile # Multi-stage build
├── docker-compose.yml # Production container config
├── .env.example # Environment variable template
└── README.md # This file
| Method | Endpoint | Description |
|---|---|---|
POST |
/auth/register |
Create a new account |
POST |
/auth/login |
Log in and receive JWT |
POST |
/auth/forgot-password |
Send password reset email |
POST |
/auth/reset-password |
Reset password with token |
| Method | Endpoint | Description |
|---|---|---|
GET |
/notes |
Get all notes for logged-in user |
POST |
/notes |
Create a new note |
GET |
/notes/:id |
Get a single note |
PUT |
/notes/:id |
Update a note |
DELETE |
/notes/:id |
Delete a note |
| Method | Endpoint | Description |
|---|---|---|
GET |
/profile |
Get user profile |
PUT |
/profile |
Update username, email, avatar |
PUT |
/profile/password |
Change password |
| Variable | Required | Description |
|---|---|---|
MONGO_URI |
Yes | MongoDB connection string (Atlas recommended) |
JWT_SECRET |
Yes | Strong random string for token signing |
UPSTASH_REDIS_REST_URL |
Yes | Upstash Redis REST endpoint |
UPSTASH_REDIS_REST_TOKEN |
Yes | Upstash Redis auth token |
PORT |
No | App port (defaults to 5001) |
RESEND_API_KEY |
Yes (for password reset) | Resend API key for transactional emails |
- Helmet-style protections via Nginx hardening in production
- Rate limiting on all API routes via Upstash Redis sliding window
- Password hashing with bcrypt (cost factor 10+)
- JWT tokens with expiration and secure HTTP-only handling
- CORS restricted to known origins in development
- Docker non-root user for container security
- DigitalOcean cloud firewall — only ports 22, 80, and 443 open
- Let's Encrypt SSL — automatic HTTPS with auto-renewal