Hi, I am using managed MongoDB database and code is written in the Nodejs, I am planning to setup SAAS multi tenancy applicaiton. How I can setup database or how should be the in architecture?
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Hi there,
This is a great question! As with everything, there are multiple ways of doing this, I recently wrote an article on how to do this with Laravel and Postgres:
Creating a Multi-Tenant Application with Laravel and Postgres
The Laravel ecosystem offers multiple packages and different approaches on how to achieve this. For MongoDB and Node.js, I’d be happy to provide some insights and recommendations for your architecture:
When it comes to multi-tenancy, there are usually three main approaches:
a) Database per Tenant:
b) Collection per Tenant:
c) Shared Collections with Tenant Field:
For a SaaS application on DigitalOcean’s Managed MongoDB, I could suggest the “Shared Collections with Tenant Field” approach. This method balances scalability and simplicity. But at the end it comes to personal preferences of course.
Here’s a basic architecture for your Node.js application:
const mongoose = require('mongoose');
const express = require('express');
const app = express();
// Middleware to extract tenantId from request (e.g., from JWT or header)
const tenantMiddleware = (req, res, next) => {
req.tenantId = extractTenantId(req);
next();
};
app.use(tenantMiddleware);
// Connect to MongoDB
mongoose.connect('your_mongodb_connection_string');
// Define a schema with tenantId
const exampleSchema = new mongoose.Schema({
tenantId: String,
// other fields...
});
// Add a pre-find hook to automatically filter by tenantId
exampleSchema.pre('find', function() {
this.where({ tenantId: this.options.tenantId });
});
const Example = mongoose.model('Example', exampleSchema);
// Route example
app.get('/data', async (req, res) => {
const data = await Example.find().setOptions({ tenantId: req.tenantId });
res.json(data);
});
app.listen(process.env.PORT || 3000);
App Platform makes it easy to deploy and scale your Node.js application:
App Platform can automatically handle scaling and load balancing for you.
Besides that, as with any application keep in mind the following:
Besides the above, here is an example on how you could achieve this architecture on the database side, we’ll use a single MongoDB database with shared collections and a tenant field for data isolation.
a) users
b) books
c) tenants
a) Tenants Collection:
{
_id: ObjectId,
name: String,
subdomain: String,
createdAt: Date,
settings: {
maxUsers: Number,
maxBooks: Number,
// other tenant-specific settings
}
}
b) Users Collection:
{
_id: ObjectId,
tenantId: ObjectId, // Reference to tenant
email: String,
password: String, // Hashed
name: String,
role: String,
createdAt: Date
}
c) Books Collection:
{
_id: ObjectId,
tenantId: ObjectId, // Reference to tenant
title: String,
author: String,
isbn: String,
publishedDate: Date,
createdAt: Date,
updatedAt: Date
}
To optimize query performance, you should create indexes on frequently queried fields:
db.users.createIndex({ tenantId: 1, email: 1 }, { unique: true })
db.books.createIndex({ tenantId: 1, isbn: 1 }, { unique: true })
db.tenants.createIndex({ subdomain: 1 }, { unique: true })
When querying the database, always include the tenantId
to ensure data isolation:
// Get all books for a specific tenant
db.books.find({ tenantId: ObjectId("tenant_id_here") })
// Get a specific user for a tenant
db.users.findOne({ tenantId: ObjectId("tenant_id_here"), email: "user@example.com" })
In your Node.js application, you can create a data access layer that automatically includes the tenantId
in all queries. Here’s a simple example:
class BookService {
constructor(tenantId) {
this.tenantId = tenantId;
}
async getAllBooks() {
return await db.books.find({ tenantId: this.tenantId }).toArray();
}
async addBook(bookData) {
const book = { ...bookData, tenantId: this.tenantId, createdAt: new Date() };
return await db.books.insertOne(book);
}
// Other methods...
}
// Usage in your application
app.get('/books', async (req, res) => {
const bookService = new BookService(req.tenantId);
const books = await bookService.getAllBooks();
res.json(books);
});
When a new tenant signs up, you’d create a new document in the tenants
collection and set up any initial data they need:
async function initializeTenant(tenantData) {
const session = await db.startSession();
session.startTransaction();
try {
const tenant = await db.tenants.insertOne(tenantData, { session });
// Create an admin user for the tenant
await db.users.insertOne({
tenantId: tenant.insertedId,
email: tenantData.adminEmail,
password: hashedPassword,
role: 'admin',
createdAt: new Date()
}, { session });
await session.commitTransaction();
return tenant.insertedId;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
If you prefer to use separate databases, here is a good article on how you could do that:
https://medium.com/geekculture/building-a-multi-tenant-app-with-nodejs-mongodb-ec9b5be6e737
Hope that this helps!
- Bobby
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.