Hey there fellow coders! Today I’m diving into implementing plan-based access control in Node.js applications – that essential system where users get different features based on their subscription tier. I’ll show you how to build feature restrictions using Express and MongoDB, with complete code examples you can adapt for your own projects. Whether you’re creating a freemium app or a multi-tiered SaaS platform, this guide has you covered!
Implementing Plan and Membership Based Access Control in Node.js Applications
Introduction: What We’ll Learn Today
Hey there fellow coders! Today I’m gonna dive into something that’s super useful for any subscription-based app or service – implementing plan and membership based access control. You know, that system where free users get some basic stuff, but premium users get all the cool features? Yeah, that one!
We’ll go through how to build this from scratch using Node.js, Express, and Mongoose. I’ll cover everything from setting up the plan model, creating middleware for access control, and even how to make your system scalable as your user base grows.
By the end of this article, you’ll have a solid understanding of how to implement access restrictions based on user plans, and have some code examples ready to use in your own projects. So let’s get started!

also checkout best services we provide with highly skilled developers team.
Understanding Plan-Based Access Control
Before we dive into the code, let’s break down what plan-based access control actually means. It’s basically a system that restricts what users can do based on the plan or subscription tier they’ve chosen.
In our example today, we’ll focus on a simple case with two plans:
- Free Plan – Users can add up to 2 products and access only basic tools
- Pro Plan – Users can add up to 10 products and access premium tools
These kinds of restrictions are super common in SaaS applications, e-commerce platforms, and virtually any service with a “freemium” model.
Setting Up Our Plan Model
First things first, we need a way to define our plans in the database. Let’s create a simple Mongoose model for this:
// models/Plan.js
const mongoose = require('mongoose');
const PlanSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
trim: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
default: 0
},
features: {
productLimit: {
type: Number,
required: true
},
hasPremiumTools: {
type: Boolean,
default: false
}
// You can add more features as needed
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Plan', PlanSchema);
This model defines the basic structure for our plans. Each plan has a name, description, price, and a set of features. For our example, we’re focusing on two features: the product limit and whether the plan has access to premium tools.
Know 2025 best js frameworks for web development.
Creating Sample Plan Data
Now let’s create some sample plan data that we can use to seed our database:
// seedData/plans.js
const plans = [
{
name: 'Free',
description: 'Basic plan with limited features',
price: 0,
features: {
productLimit: 2,
hasPremiumTools: false
},
isActive: true
},
{
name: 'Pro',
description: 'Premium plan with all features',
price: 9.99,
features: {
productLimit: 10,
hasPremiumTools: true
},
isActive: true
}
];
module.exports = plans;
To seed this data into our database, we can create a simple script:
// scripts/seedPlans.js
const mongoose = require('mongoose');
const Plan = require('../models/Plan');
const plans = require('../seedData/plans');
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
async function seedPlans() {
try {
// Clear existing plans
await Plan.deleteMany({});
// Insert new plans
const createdPlans = await Plan.insertMany(plans);
console.log(`${createdPlans.length} plans created successfully!`);
mongoose.connection.close();
} catch (error) {
console.error('Error seeding plans:', error);
mongoose.connection.close();
}
}
seedPlans();
User Model with Plan Reference
Next, we need to modify our User model to include a reference to the plan they’ve subscribed to:
// models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
trim: true
},
password: {
type: String,
required: true
},
plan: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Plan',
required: true
},
planStartDate: {
type: Date,
default: Date.now
},
planEndDate: {
type: Date
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', UserSchema);
Creating Access Control Utilities
Now comes the fun part – implementing the access control logic. We’ll create a utility function that checks if a user has access to a specific feature based on their plan:
// utils/accessControl.js
const User = require('../models/User');
/**
* Check if user has reached their product limit
* @param {string} userId - The user ID
* @param {number} currentProductCount - Current number of products user has
* @returns {Promise<boolean>} - True if user can add more products
*/
async function canAddProduct(userId, currentProductCount) {
try {
const user = await User.findById(userId).populate('plan');
if (!user) {
throw new Error('User not found');
}
return currentProductCount < user.plan.features.productLimit;
} catch (error) {
console.error('Error checking product limit:', error);
return false;
}
}
/**
* Check if user has access to premium tools
* @param {string} userId - The user ID
* @returns {Promise<boolean>} - True if user has access to premium tools
*/
async function hasPremiumAccess(userId) {
try {
const user = await User.findById(userId).populate('plan');
if (!user) {
throw new Error('User not found');
}
return user.plan.features.hasPremiumTools;
} catch (error) {
console.error('Error checking premium access:', error);
return false;
}
}
module.exports = {
canAddProduct,
hasPremiumAccess
};
Creating Middleware for Route Protection
To make our access control system more usable, let’s create some Express middleware to protect routes based on plan features:
// middleware/planCheck.js
const { canAddProduct, hasPremiumAccess } = require('../utils/accessControl');
const Product = require('../models/Product');
/**
* Middleware to check if user can add a new product
*/
async function checkProductLimit(req, res, next) {
try {
const userId = req.user.id; // Assuming you have authentication middleware that sets req.user
// Count user's existing products
const productCount = await Product.countDocuments({ user: userId });
// Check if user can add more products
const canAdd = await canAddProduct(userId, productCount);
if (!canAdd) {
return res.status(403).json({
success: false,
message: 'Product limit reached for your plan. Please upgrade to add more products.'
});
}
next();
} catch (error) {
console.error('Product limit check error:', error);
res.status(500).json({ success: false, message: 'Server error' });
}
}
/**
* Middleware to check if user has premium access
*/
async function checkPremiumAccess(req, res, next) {
try {
const userId = req.user.id; // Assuming you have authentication middleware
// Check if user has premium access
const hasPremium = await hasPremiumAccess(userId);
if (!hasPremium) {
return res.status(403).json({
success: false,
message: 'This feature requires a premium plan. Please upgrade to access premium tools.'
});
}
next();
} catch (error) {
console.error('Premium access check error:', error);
res.status(500).json({ success: false, message: 'Server error' });
}
}
module.exports = {
checkProductLimit,
checkPremiumAccess
};
Implementing the Routes
Now let’s put it all together by implementing some routes that use our middleware:
// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
const { checkProductLimit } = require('../middleware/planCheck');
const Product = require('../models/Product');
// @route POST /api/products
// @desc Create a new product
// @access Private + Plan Check
router.post('/', [auth, checkProductLimit], async (req, res) => {
try {
const { name, description, price } = req.body;
const newProduct = new Product({
name,
description,
price,
user: req.user.id
});
const product = await newProduct.save();
res.json(product);
} catch (error) {
console.error('Error creating product:', error);
res.status(500).send('Server error');
}
});
// Other product routes...
module.exports = router;
// routes/toolRoutes.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
const { checkPremiumAccess } = require('../middleware/planCheck');
// @route GET /api/tools/premium
// @desc Access premium tools
// @access Private + Premium Check
router.get('/premium', [auth, checkPremiumAccess], async (req, res) => {
try {
// Premium tools logic here
res.json({
success: true,
message: 'Welcome to premium tools!',
tools: [
{ id: 1, name: 'Advanced Analytics' },
{ id: 2, name: 'Bulk Editor' },
{ id: 3, name: 'AI Recommendations' }
]
});
} catch (error) {
console.error('Error accessing premium tools:', error);
res.status(500).send('Server error');
}
});
// Other tool routes...
module.exports = router;
Configuring the App
Finally, let’s set up the main Express app:
// app.js
const express = require('express');
const mongoose = require('mongoose');
const productRoutes = require('./routes/productRoutes');
const toolRoutes = require('./routes/toolRoutes');
const planRoutes = require('./routes/planRoutes');
const app = express();
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Middleware
app.use(express.json());
// Routes
app.use('/api/products', productRoutes);
app.use('/api/tools', toolRoutes);
app.use('/api/plans', planRoutes);
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Making Your Plan-Based Access System Scalable and Fast
Now, let’s talk about how to make this system more scalable and performant for when your user base grows. Here are some tips:
1. Implement Caching
One of the biggest bottlenecks in our current implementation is that we’re querying the database every time we need to check a user’s plan. Let’s use Redis to cache user plan information:
// utils/accessControlWithCache.js
const Redis = require('ioredis');
const User = require('../models/User');
// Create Redis client
const redis = new Redis({
host: 'localhost',
port: 6379
});
/**
* Get user's plan features from cache or database
* @param {string} userId - The user ID
* @returns {Promise<Object>} - Plan features
*/
async function getUserPlanFeatures(userId) {
try {
// Try to get from cache first
const cacheKey = `user:${userId}:plan`;
const cachedPlan = await redis.get(cacheKey);
if (cachedPlan) {
return JSON.parse(cachedPlan);
}
// If not in cache, get from database
const user = await User.findById(userId).populate('plan');
if (!user) {
throw new Error('User not found');
}
// Store in cache for 1 hour
await redis.set(
cacheKey,
JSON.stringify(user.plan.features),
'EX',
3600
);
return user.plan.features;
} catch (error) {
console.error('Error getting user plan:', error);
throw error;
}
}
/**
* Check if user has reached their product limit
* @param {string} userId - The user ID
* @param {number} currentProductCount - Current number of products user has
* @returns {Promise<boolean>} - True if user can add more products
*/
async function canAddProduct(userId, currentProductCount) {
try {
const features = await getUserPlanFeatures(userId);
return currentProductCount < features.productLimit;
} catch (error) {
console.error('Error checking product limit:', error);
return false;
}
}
/**
* Check if user has access to premium tools
* @param {string} userId - The user ID
* @returns {Promise<boolean>} - True if user has access to premium tools
*/
async function hasPremiumAccess(userId) {
try {
const features = await getUserPlanFeatures(userId);
return features.hasPremiumTools;
} catch (error) {
console.error('Error checking premium access:', error);
return false;
}
}
/**
* Invalidate user plan cache when plan changes
* @param {string} userId - The user ID
*/
async function invalidateUserPlanCache(userId) {
const cacheKey = `user:${userId}:plan`;
await redis.del(cacheKey);
}
module.exports = {
canAddProduct,
hasPremiumAccess,
invalidateUserPlanCache
};
2. Add Cache Invalidation When Plan Changes
When a user upgrades or changes their plan, we need to invalidate the cache:
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
const User = require('../models/User');
const { invalidateUserPlanCache } = require('../utils/accessControlWithCache');
// @route PUT /api/users/plan
// @desc Update user's plan
// @access Private
router.put('/plan', auth, async (req, res) => {
try {
const { planId } = req.body;
// Update user's plan
const user = await User.findByIdAndUpdate(
req.user.id,
{
plan: planId,
planStartDate: Date.now()
},
{ new: true }
);
// Invalidate cache
await invalidateUserPlanCache(req.user.id);
res.json(user);
} catch (error) {
console.error('Error updating plan:', error);
res.status(500).send('Server error');
}
});
module.exports = router;
3. Use Background Workers for Plan Status Updates
To handle plan expirations and renewals more efficiently, we can use a background worker:
// workers/planStatusWorker.js
const cron = require('node-cron');
const User = require('../models/User');
const Plan = require('../models/Plan');
const { invalidateUserPlanCache } = require('../utils/accessControlWithCache');
// Run every day at midnight
cron.schedule('0 0 * * *', async () => {
try {
console.log('Running plan status update worker...');
// Find users with expired plans
const expiredUsers = await User.find({
planEndDate: { $lt: new Date() }
});
// Get free plan
const freePlan = await Plan.findOne({ name: 'Free' });
if (!freePlan) {
throw new Error('Free plan not found');
}
// Update expired users to free plan
for (const user of expiredUsers) {
user.plan = freePlan._id;
await user.save();
// Invalidate cache
await invalidateUserPlanCache(user._id);
console.log(`User ${user._id} reverted to free plan`);
}
console.log('Plan status update completed');
} catch (error) {
console.error('Plan status update error:', error);
}
});
4. Implement Database Indexing
To improve database query performance, add the right indexes:
// models/User.js (updated)
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
trim: true
},
password: {
type: String,
required: true
},
plan: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Plan',
required: true,
index: true // Add index here
},
planStartDate: {
type: Date,
default: Date.now
},
planEndDate: {
type: Date,
index: true // Add index here
},
createdAt: {
type: Date,
default: Date.now
}
});
// Add compound index for queries that might filter by both user and plan
UserSchema.index({ plan: 1, planEndDate: 1 });
module.exports = mongoose.model('User', UserSchema);
5. Implement Rate Limiting
To prevent abuse of your API, add rate limiting:
// middleware/rateLimit.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379
});
// Create rate limiter
const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rate-limit:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false
});
module.exports = apiLimiter;
Then apply it to your routes:
// app.js (updated)
const express = require('express');
const mongoose = require('mongoose');
const apiLimiter = require('./middleware/rateLimit');
const productRoutes = require('./routes/productRoutes');
const toolRoutes = require('./routes/toolRoutes');
const planRoutes = require('./routes/planRoutes');
const app = express();
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Middleware
app.use(express.json());
// Apply rate limiter to all API routes
app.use('/api/', apiLimiter);
// Routes
app.use('/api/products', productRoutes);
app.use('/api/tools', toolRoutes);
app.use('/api/plans', planRoutes);
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Tips for Making Your Plan-Based Access System Better
Here are some additional tips to make your system even better:
1. Implement a Feature Flags System
Instead of hardcoding features in the plan model, create a more flexible system using feature flags:
// models/Plan.js (updated with feature flags)
const mongoose = require('mongoose');
const PlanSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
trim: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
default: 0
},
features: {
type: Map,
of: mongoose.Schema.Types.Mixed,
default: {}
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Plan', PlanSchema);
With this approach, you can dynamically add or remove features without changing your schema.
2. Add Usage Tracking
Track how many resources users are consuming:
// models/UserUsage.js
const mongoose = require('mongoose');
const UserUsageSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
productCount: {
type: Number,
default: 0
},
apiCallsCount: {
type: Number,
default: 0
},
premiumToolsUsage: {
type: Number,
default: 0
},
month: {
type: Number,
required: true
},
year: {
type: Number,
required: true
},
lastUpdated: {
type: Date,
default: Date.now
}
});
// Compound index for querying by user and time period
UserUsageSchema.index({ user: 1, year: 1, month: 1 }, { unique: true });
module.exports = mongoose.model('UserUsage', UserUsageSchema);
3. Use Server-Side Feature Flags
For even more flexibility, use a feature flags service:
// utils/featureFlags.js
const LaunchDarkly = require('launchdarkly-node-server-sdk');
// Initialize LaunchDarkly client
const ldClient = LaunchDarkly.init('YOUR_SDK_KEY');
/**
* Check if a feature is enabled for a user
* @param {Object} user - User object
* @param {string} featureKey - Feature flag key
* @param {boolean} defaultValue - Default value if flag is not found
* @returns {Promise<boolean>} - True if feature is enabled
*/
async function isFeatureEnabled(user, featureKey, defaultValue = false) {
try {
await ldClient.waitForInitialization();
const context = {
kind: 'user',
key: user._id.toString(),
name: user.name,
email: user.email,
custom: {
planId: user.plan.toString(),
planName: user.planName
}
};
return await ldClient.variation(featureKey, context, defaultValue);
} catch (error) {
console.error('Feature flag error:', error);
return defaultValue;
}
}
module.exports = {
isFeatureEnabled
};
4. Implement a Monthly Usage Reset Job
To reset usage counters at the beginning of each month:
// workers/usageResetWorker.js
const cron = require('node-cron');
const UserUsage = require('../models/UserUsage');
// Run at midnight on the first day of each month
cron.schedule('0 0 1 * *', async () => {
try {
console.log('Creating new usage records for the month...');
const now = new Date();
const month = now.getMonth() + 1;
const year = now.getFullYear();
// Get all users from the previous month
const lastMonth = month === 1 ? 12 : month - 1;
const lastYear = month === 1 ? year - 1 : year;
const previousUsages = await UserUsage.find({
month: lastMonth,
year: lastYear
});
// Create new records for the current month
const newUsages = previousUsages.map(usage => ({
user: usage.user,
productCount: 0, // Reset to 0
apiCallsCount: 0, // Reset to 0
premiumToolsUsage: 0, // Reset to 0
month,
year,
lastUpdated: now
}));
if (newUsages.length > 0) {
await UserUsage.insertMany(newUsages);
console.log(`Created ${newUsages.length} new usage records`);
}
console.log('Usage reset completed');
} catch (error) {
console.error('Usage reset error:', error);
}
});
Conclusion: Building a Robust Plan-Based Access Control System
We’ve covered a lot of ground in this article! We started with a simple implementation of plan-based access control and then explored ways to make it more scalable and performant.
Here’s a recap of what we learned:
- How to create a basic plan model with different features and limitations
- How to implement access control middleware to restrict access based on plan
- How to use caching to improve performance
- How to implement cache invalidation when plans change
- How to use background workers for plan status updates
- How to optimize database queries with indexing
- How to add rate limiting to protect your API
- How to make your system more flexible with feature flags
- How to track usage and implement monthly resets
Remember, the implementation you choose should depend on your specific needs. For smaller applications, the basic implementation might be enough. But as your application grows, you’ll want to implement more advanced features like caching and background workers.
I hope this article helped you understand how to implement plan and membership based access control in your Node.js applications. Feel free to adapt these examples to fit your specific use case.
Happy coding!
Meta Title: Implementing Plan and Membership Based Access Control in Node.js | Complete Guide Meta Description: Learn how to build a scalable plan-based access control system with Node.js, Express, and MongoDB. Includes code examples and best practices for caching and performance optimization.