Skip to content

Commit 0d0d706

Browse files
committed
fix: resolve 'project.save' error and optimize auth middleware
- fix(data.controller): replace project.save() with atomic Project.updateOne() to support lean objects - perf(middleware): optimize verifyApiKey to select only necessary fields - docs: update DEVLOG.md with recent debugging and optimization notes
1 parent 8d788ff commit 0d0d706

3 files changed

Lines changed: 159 additions & 2 deletions

File tree

‎DEVLOG.md‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# urBackend Development Log
2+
3+
**urBackend**: [https://urbackend.bitbros.in](https://urbackend.bitbros.in)
4+
**Github**: [https://github.com/yash-pouranik/urbackend](https://github.com/yash-pouranik/urbackend)
5+
6+
---
7+
8+
## 16-01-2026 - Optimization: User Dashboard Projects Query
9+
10+
### Context
11+
On the user dashboard, I display a list of all projects owned by the logged-in user. The UI only requires the project name and description, but the backend query was returning the full Project document.
12+
13+
### Original Issue
14+
* Query fetched **all fields** from the Project model (≈15 fields)
15+
* Unnecessary data transfer and Mongoose document hydration
16+
* Not optimal for a frequently hit dashboard endpoint
17+
18+
### Changes Implemented
19+
```javascript
20+
const projects = await Project.find({ owner: req.user._id })
21+
.select('name description')
22+
.lean();
23+
```
24+
25+
Additionally, I created an index:
26+
```javascript
27+
projectSchema.index({ owner: 1 });
28+
```
29+
30+
### Why This Improves Performance
31+
* **`select('name description')`**: Reduces payload size and serialization cost
32+
* **`.lean()`**: Skips Mongoose document hydration (plain JS objects)
33+
* **Index on owner**: Avoids collection scans and significantly improves query speed
34+
35+
### Key Learning
36+
* `select()` optimizes data transfer, not query execution
37+
* **Indexes** drive real query speed
38+
* `.lean()` is critical for read-heavy, display-only endpoints
39+
40+
### Result
41+
* Faster dashboard load time
42+
* Lower memory and CPU usage on the backend
43+
* Cleaner and safer API response
44+
45+
---
46+
47+
## 16-01-2026 - API Key Middleware Redis Caching
48+
49+
### Context
50+
The API-key authentication middleware runs on every incoming request. Originally, each request queried MongoDB to validate the API key and check the project owner’s verification status, making this a hot path and a potential performance bottleneck.
51+
52+
### Problem
53+
* Repeated MongoDB queries for the same API key
54+
* Increased latency under high request volume
55+
* Unnecessary load on the database for read-heavy authentication checks
56+
57+
### Optimization Implemented
58+
I introduced Redis caching for API-key validation.
59+
60+
#### Cache Strategy
61+
* **Redis String**: JSON-serialized object
62+
* **Cache key**: `project:apikey:{hashedApiKey}`
63+
* **Cached data**:
64+
* `projectId`
65+
* `owner.isVerified`
66+
* Minimal project metadata required by middleware
67+
* **TTL**: 2 hours
68+
69+
#### Updated Flow
70+
1. Hash incoming API key
71+
2. Check Redis for cached project
72+
3. On cache hit > skip MongoDB
73+
4. On cache miss > query MongoDB, cache result, continue
74+
5. Verify owner status before allowing request
75+
76+
### Why Redis String
77+
* Entire project object is read at once
78+
* No partial updates required
79+
* Simple GET / SET pattern
80+
* Easy invalidation
81+
82+
### Cache Invalidation Strategy
83+
Cache is explicitly invalidated when:
84+
* API key is rotated
85+
* Project is deleted
86+
* Project ownership or verification status changes
87+
88+
### Key Learnings
89+
* Middleware is a high-impact caching target
90+
* `select()` optimizes payload size, not query execution
91+
* Redis significantly reduces DB load for auth-heavy systems
92+
* Correct invalidation is critical for security
93+
94+
### Result
95+
* Reduced authentication latency
96+
* Lower MongoDB query volume
97+
* More scalable API-key validation path
98+
99+
---
100+
101+
## 17-01-2026 - Handling Write Operations with RContext
102+
103+
### Context
104+
After introducing Redis caching for API-key middleware, `req.project` started coming from Redis instead of MongoDB. Redis returns plain JavaScript objects, not hydrated Mongoose documents.
105+
106+
### Problem
107+
Some controllers (e.g., `insertData`) perform write operations on the Project model, such as updating `databaseUsed`.
108+
When the project object came from Redis:
109+
> `project.save is not a function`
110+
111+
This happened because cached objects do not have Mongoose instance methods.
112+
113+
### Initial Considerations
114+
I evaluated multiple approaches:
115+
* Skipping `.save()` (unsafe, loses consistency)
116+
* Rehydrating cached objects into Mongoose documents (error-prone)
117+
* Moving writes to background jobs (correct but premature)
118+
* Refetching the document on every write (works but adds overhead)
119+
120+
### Final Solution
121+
I replaced the read-modify-write pattern with **atomic MongoDB updates**.
122+
123+
```javascript
124+
await Project.updateOne(
125+
{ _id: project._id },
126+
{ $inc: { databaseUsed: docSize } }
127+
);
128+
```
129+
130+
### Why This Works
131+
* Avoids document hydration
132+
* Single database call
133+
* Atomic and concurrency-safe
134+
* Keeps MongoDB as the source of truth
135+
* Cache remains strictly read-only
136+
137+
### Key Learning
138+
* Redis caching must be treated as a **read optimization**.
139+
* All mutations should go through the database using atomic operations.
140+
141+
### Result
142+
* No runtime crashes
143+
* Lower database overhead
144+
* Clear separation between read and write paths
145+
* More scalable quota enforcement logic

‎backend/controllers/data.controller.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ module.exports.insertData = async (req, res) => {
5555
const result = await Model.create(safeData);
5656

5757
if (!project.resources.db.isExternal) {
58-
project.databaseUsed = (project.databaseUsed || 0) + docSize;
59-
await project.save();
58+
await Project.updateOne(
59+
{ _id: cachedProject._id },
60+
{ $inc: { databaseUsed: docSize } }
61+
);
6062
}
6163

6264
res.status(201).json(result);

‎backend/middleware/verifyApiKey.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,19 @@ module.exports = async (req, res, next) => {
1818

1919
if (!project) {
2020
project = await Project.findOne({ apiKey: hashedApi })
21+
.select(`
22+
owner
23+
resources
24+
collections
25+
databaseLimit
26+
databaseUsed
27+
storageLimit
28+
storageUsed
29+
`)
2130
.populate('owner', 'isVerified')
2231
.lean();
2332

33+
2434
if (!project) {
2535
return res.status(401).json({
2636
error: "API key is expired or invalid.",

0 commit comments

Comments
 (0)