The YouTube Subscription Model
Think about how YouTube subscriptions work:
- A channel on YouTube is just a user account
- When user A "subscribes" to user B's channel, there is a relationship between A (subscriber) and B (channel)
- Unsubscribing removes that relationship
The key insight is that there is no separate Channel entity. Channels are just users. So the Subscription document simply records: "this user subscribed to this other user."
---
Subscription Schema Design
const subscriptionSchema = new Schema(
{
subscriber: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
channel: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
},
{ timestamps: true }
);
// Compound unique index: a user can only subscribe to a channel once
subscriptionSchema.index({ subscriber: 1, channel: 1 }, { unique: true });
Both subscriber and channel reference the same User collection — they are both user IDs. The field names clarify the role.
---
Counting Subscribers of a Channel
// How many users have subscribed to a channel?
const subscriberCount = await Subscription.countDocuments({ channel: channelId });
If user B has 1000 subscribers, that means there are 1000 Subscription documents where channel === userB._id.
---
Counting Channels a User Subscribes To
// How many channels has this user subscribed to?
const subscriptionCount = await Subscription.countDocuments({ subscriber: userId });
---
Toggle Subscribe / Unsubscribe
const toggleSubscription = asyncHandler(async (req, res) => {
const { channelId } = req.params;
const subscriberId = req.user._id;
if (channelId.toString() === subscriberId.toString()) {
throw new ApiError(400, 'You cannot subscribe to your own channel');
}
const existingSubscription = await Subscription.findOne({
subscriber: subscriberId,
channel: channelId,
});
let message;
if (existingSubscription) {
// Already subscribed — unsubscribe
await Subscription.findByIdAndDelete(existingSubscription._id);
message = 'Unsubscribed successfully';
} else {
// Not subscribed — subscribe
await Subscription.create({ subscriber: subscriberId, channel: channelId });
message = 'Subscribed successfully';
}
return res.status(200).json(new ApiResponse(200, {}, message));
});
---
Aggregation: Get Both Counts + isSubscribed in One Query
Instead of three separate database queries, use aggregation to get everything at once:
const channelStats = await User.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(channelId) } },
// Join: get all subscribers of this channel
{
$lookup: {
from: 'subscriptions',
localField: '_id',
foreignField: 'channel',
as: 'subscribers',
},
},
// Join: get all channels this user subscribes to
{
$lookup: {
from: 'subscriptions',
localField: '_id',
foreignField: 'subscriber',
as: 'subscribedTo',
},
},
{
$addFields: {
subscribersCount: { $size: '$subscribers' },
subscribedToCount: { $size: '$subscribedTo' },
// Check if the requesting user is in the subscribers list
isSubscribed: {
$in: [new mongoose.Types.ObjectId(req.user._id), '$subscribers.subscriber'],
},
},
},
{
$project: {
subscribersCount: 1,
subscribedToCount: 1,
isSubscribed: 1,
},
},
]);
This pattern powers the channel profile page: subscriber count, who the channel subscribes to, and a subscribe/unsubscribe button — all from a single aggregation.
---
Why This Design Avoids a Separate Channel Collection
Some developers create a separate Channel collection, but this leads to:
- Data duplication (user name/avatar stored in both User and Channel)
- Complex sync logic (update both User and Channel when profile changes)
- More complex queries (join User → Channel → Subscription)
With the "subscriptions are user-to-user" design:
- One source of truth (User collection)
- Subscriptions are lightweight relationship documents
- Easy to query: filter by subscriber or channel field
---
Index Strategy for Performance
| Index | Query it Supports |
|---|---|
{ subscriber: 1 } | "What channels does this user subscribe to?" |
{ channel: 1 } | "Who subscribes to this channel?" |
{ subscriber: 1, channel: 1 } (unique) | Toggle subscribe + prevent duplicates |