A production-ready financial backend built with Node.js, Express, and MongoDB — featuring role-based access control, JWT authentication, and AI-powered financial insights.
🔗 Live Demo: https://ledgerly-navy.vercel.app 📦 Backend Repo: https://github.com/shivam22-source/Backend-Ledgerly
When I read the Zorvyn assignment brief, I realized I had already built exactly this system.
Ledgerly is a full-stack personal finance tracker I built from scratch — it covers every core requirement in the assignment:
- ✅ User management with role-based access (viewer / analyst / admin)
- ✅ Financial records with full CRUD + filtering
- ✅ Dashboard summary APIs using MongoDB aggregation pipelines
- ✅ JWT authentication with access + refresh token flow
- ✅ Input validation, error handling, and rate limiting
- ✅ Soft delete for data safety
- ✅ Unit tests with Chai
- ✅ AI-powered financial assistant using Gemini API (bonus)
The only thing missing from my original build was explicit role-based authorization on routes — which I have now added as part of this submission.
Client Request
│
▼
Rate Limiter (Login only)
│
▼
JWT Auth Middleware ──► 401 if no/invalid token
│
▼
Role Authorization ──► 403 if insufficient role
│
▼
Input Validation (Joi) ──► 400 if invalid data
│
▼
Route Handler
│
▼
MongoDB (Mongoose)
| Role | View Transactions | Create Transaction | Delete Transaction | View Summary |
|---|---|---|---|---|
| viewer | ✅ | ❌ | ❌ | ✅ |
| analyst | ✅ | ✅ | ❌ | ✅ |
| admin | ✅ | ✅ | ✅ | ✅ |
Roles are embedded in the JWT payload at login and verified on every protected route via authorization.js middleware.
POST /api/auth/register
└── Validate input (Joi)
└── Hash password (bcrypt, 10 rounds)
└── Save user with default role: "viewer"
POST /api/auth/login
└── Rate limited (3 attempts / 15 min)
└── Verify credentials
└── Issue accessToken (15m) + refreshToken (7d)
└── Both tokens carry { userId, role } payload
POST /api/auth/refresh
└── Verify refreshToken from DB
└── Issue new accessToken
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/auth/register |
Public | Register new user |
| POST | /api/auth/login |
Public | Login + get tokens |
| POST | /api/auth/refresh |
Public | Refresh access token |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/transaction |
analyst, admin | Create transaction |
| GET | /api/transaction-view |
viewer, analyst, admin | View all transactions |
| POST | /api/transaction-del/:id |
admin only | Soft delete transaction |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/balance |
viewer, analyst, admin | Lifetime balance |
| GET | /api/balance-month |
viewer, analyst, admin | Monthly debit/credit summary |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/ai-chat |
All authenticated | Gemini AI finance advisor |
1. Soft Delete over Hard Delete
Transactions use an isDeleted flag instead of permanent removal. Financial data should never be lost — this mirrors real-world accounting practices.
2. MongoDB Aggregation for Summaries
Balance and monthly summaries are computed using MongoDB's aggregation pipeline ($match → $group → $sum) rather than fetching all records and calculating in JavaScript. This is significantly more efficient at scale.
3. Separation of Concerns Transaction data and aggregated financial insights are handled by separate API endpoints — keeping logic clean and each endpoint single-purpose.
4. Role in JWT Payload The user's role is embedded directly in the JWT token payload. This means every protected route can check permissions without an extra DB lookup on every request.
5. Refresh Token stored in DB Refresh tokens are stored on the user document, enabling server-side invalidation (logout from all devices) — a security practice missing from most basic JWT implementations.
| Layer | Technology |
|---|---|
| Runtime | Node.js |
| Framework | Express.js |
| Database | MongoDB + Mongoose |
| Auth | JWT (jsonwebtoken) |
| Password | bcrypt |
| Validation | Joi |
| Rate Limiting | express-rate-limit |
| Testing | Chai + chai-http |
| AI | Google Gemini API |
| Deployment | Render + MongoDB Atlas |
# 1. Clone the repo
git clone https://github.com/shivam22-source/Backend-Ledgerly
cd Backend-Ledgerly
# 2. Install dependencies
npm install
# 3. Create .env file
cp .env.example .env
# Fill in your values
# 4. Start server
node app.jsMONGO_URI=your_mongodb_connection_string
ACCESS_TOKEN_SECRET=your_access_secret
REFRESH_TOKEN_SECRET=your_refresh_secret
GEMINI_API_KEY=your_gemini_api_keynpm testTests cover:
- User registration (POST /api/auth/register)
- User login + token response (POST /api/auth/login)
├── config/
│ └── mongoose.js # DB connection
├── middleware/
│ ├── auth.js # JWT verification
│ ├── authorization.js # Role-based access control
│ ├── validate.js # Joi validation wrapper
│ └── rate-limit.js # Login rate limiter
├── models/
│ ├── user.model.js # User schema (with roles)
│ └── task.model.js # Transaction schema
├── validators/
│ └── user.validator.js # Joi schemas
├── utils/
│ └── token.js # JWT generator functions
├── tests/
│ └── auth.test.js # Chai unit tests
├── .env.example
└── app.js # Main server + all routes
- Default role on registration is
viewer— admin accounts must be manually assigned in DB (or via a separate admin-only endpoint) - Soft delete is used for transactions; hard delete is not supported by design
- Monthly summary uses calendar month boundaries (1st to last day)
- AI chat endpoint passes user's actual transaction context to Gemini for personalized advice
Built by Shivam Thakur — B.Tech IT, IIIT Bhubaneswar