Designing the User Model for a YouTube-like App
The User model is the most critical schema in your backend. It needs to support authentication (password hashing, JWT tokens), profile management (avatar, cover image), and relationships (watch history, subscriptions). Let us design it properly from the start.
---
Full User Schema Fields
| Field | Type | Purpose |
|---|---|---|
username | String, unique, lowercase | URL-friendly unique identifier |
email | String, unique, lowercase | Login identifier |
fullName | String | Display name |
password | String, select: false | Hashed with bcrypt |
avatar | String | Cloudinary URL for profile picture |
coverImage | String | Cloudinary URL for channel banner |
watchHistory | [ObjectId ref Video] | Array of watched video IDs |
refreshToken | String, select: false | Stored for token rotation |
createdAt, updatedAt | Date | Added by timestamps option |
---
Full Video Schema Fields
| Field | Type | Purpose |
|---|---|---|
videoFile | String | Cloudinary URL for the video |
thumbnail | String | Cloudinary URL for video thumbnail |
title | String, required | Video title |
description | String | Video description |
duration | Number | Duration in seconds (from Cloudinary) |
views | Number, default 0 | View count |
isPublished | Boolean, default true | Draft vs published |
owner | ObjectId ref User | Who uploaded the video |
createdAt, updatedAt | Date | Timestamps |
---
Pre-save Hook: Password Hashing
The pre-save hook runs before a document is saved to MongoDB. We use it to hash the password automatically whenever it is created or changed:
userSchema.pre('save', async function (next) {
// Only hash if the password field was actually modified
// This prevents re-hashing an already-hashed password on every save
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
// 10 = salt rounds. Higher = more secure but slower.
// 10 is the industry standard — takes ~100ms on modern hardware
next();
});
Why 10 salt rounds? bcrypt generates a unique salt for each hash, preventing rainbow table attacks. 10 rounds means bcrypt runs 2^10 = 1024 iterations. This makes brute-force attacks computationally expensive.
---
isPasswordCorrect Instance Method
userSchema.methods.isPasswordCorrect = async function (password) {
// this.password is the hashed password from the database
// bcrypt.compare() hashes the candidate password and compares
return await bcrypt.compare(password, this.password);
};
You need to explicitly select the password field when querying:User.findOne({ email }).select('+password')because we setselect: falseon the password field.
---
JWT: Access Token vs Refresh Token
Why two tokens?
- Access Token: Short-lived (15 minutes to 1 hour). Sent with every API request. If stolen, attacker can only use it briefly before it expires.
- Refresh Token: Long-lived (7-30 days). Stored in an httpOnly cookie and in the database. Used only to generate new access tokens. Storing it in the DB allows revocation — if a user logs out or the token is suspected stolen, delete it from DB.
---
generateAccessToken Method
userSchema.methods.generateAccessToken = function () {
return jwt.sign(
{
_id: this._id,
email: this.email,
username: this.username,
fullName: this.fullName,
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRY || '15m' }
);
};
The JWT payload contains just enough info to identify the user and populate req.user in middleware without a DB query for every request.
---
generateRefreshToken Method
userSchema.methods.generateRefreshToken = function () {
return jwt.sign(
{ _id: this._id }, // Minimal payload — only need _id to look up user
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRY || '7d' }
);
};
The refresh token has only _id in the payload because it is used purely to generate a new access token — we immediately look up the full user from the database.
---
Why Store Refresh Token in the Database?
| Benefit | Explanation |
|---|---|
| Revocation | Delete from DB on logout — even valid tokens won't work |
| Rotation | Issue new refresh token on each use, invalidate old one |
| Multi-device logout | Can clear all refresh tokens for a user (logout from all devices) |
| Security audit | Can track when refresh tokens were issued and used |
---
JWT Payload Design Principles
- Include only what is needed to serve requests without a DB hit
- Do NOT include sensitive data (password, credit card, etc.)
- Access token payload:
{ _id, email, username, fullName, role } - Refresh token payload:
{ _id }only - JWT is base64url-encoded (not encrypted) — anyone can decode the payload; sign ensures integrity