Quick Start Guide
Introduction
What is it? @node-tenant/tenant is a comprehensive multi-tenant framework for Node.js. Multi-tenancy is a software architecture where a single instance of a software application serves multiple customers (called "tenants").
Why do we need it? If you are building a SaaS (Software as a Service) application, you need to make sure Customer A cannot see Customer B's data. Instead of building complex permissions from scratch, this framework isolates data automatically at the database level.
Where is it used? B2B platforms, SaaS dashboards, white-label products, and any system where user data belongs to distinct organizational boundaries.
Real-world Analogy
Imagine an apartment building. The building itself (the foundation, the plumbing, the electricity) is your application. Each individual apartment is a tenant. All apartments share the same building infrastructure, but every tenant has their own key, and they cannot enter someone else's apartment. This framework is the "lock and key" system for your database.
Prerequisites
- Basic knowledge of Node.js and building APIs.
- Familiarity with an HTTP framework like Express or Next.js.
- Basic understanding of databases and ORMs (like Prisma, TypeORM, or Mongoose).
1. Installation
To get started, you need to install a few packages. Our framework is highly modular, meaning you only install what you actually use. Select your preferred package manager:
# Install the core engine (required for everything)
pnpm add @node-tenant/tenant-core
# Install the Express framework adapter (so we can intercept HTTP requests)
pnpm add @node-tenant/tenant-express
# Install the Prisma storage adapter (tells the system where to save tenant lists)
pnpm add @node-tenant/tenant-storage-prisma
# Install the Prisma ORM adapter (automatically filters database queries)
pnpm add @node-tenant/tenant-prisma
2. Simple Integration Example
Below is a quick setup utilizing Express and Prisma. This code sets up a server that automatically identifies which customer is making a request based on the URL they visit.
import express from 'express';
import { PrismaClient } from '@prisma/client';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, subdomainResolver } from '@node-tenant/tenant-express';
import { createPrismaStorage } from '@node-tenant/tenant-storage-prisma';
import { useTenantPrisma } from '@node-tenant/tenant-prisma';
// 1. Initialize Database & Storage
const prisma = new PrismaClient();
const storage = createPrismaStorage(prisma);
const manager = new TenantManager({ storage });
// 2. Initialize Express App
const app = express();
app.use(express.json());
// 3. Register Tenant Middleware
// Resolves tenant from subdomain: customer1.myapp.com -> tenant slug "customer1"
app.use(tenantMiddleware({ manager, resolver: subdomainResolver(storage) }));
// 4. Create Protected Route
app.get('/api/users', async (req, res) => {
// 5. Fetch Data
const db = useTenantPrisma(prisma);
const users = await db.user.findMany();
res.json(users);
});
app.listen(3000, () => console.log('Server is running on port 3000'));
Line-by-Line Explanation
Step 1: We initialize the standard Prisma Client. Then, we pass it to createPrismaStorage, which tells our Tenant Manager to save the list of customers in our Prisma database.
Step 3: tenantMiddleware is a function that runs on every HTTP request. subdomainResolver looks at the URL (e.g., acme.myapp.com) and extracts "acme". It then fetches the tenant data for "acme" from the database and saves it in a hidden memory space called AsyncLocalStorage for the duration of the request.
Step 5: Inside the route, we don't use the standard prisma client. We use useTenantPrisma(prisma). Because the middleware saved "acme" in memory, this wrapped client automatically adds WHERE tenantId = 'acme' to the database query! You don't have to manually filter data ever again.
Best Practices for Beginners
- Always use the scoped client: Once you set this up, never use the raw
PrismaClientinside your routes. Always use the scopeduseTenantPrismaclient to ensure data doesn't leak. - Environment Variables: Ensure your
DATABASE_URLis properly configured in a.envfile before running this code.
Summary
- The framework uses middleware to detect who is making the request (e.g., via subdomains).
- It saves this context globally for that specific request.
- Database adapters read this context and automatically filter queries to keep data secure.
Packages Directory
The @node-tenant/* ecosystem is split into 21 modular packages.Why split it into so many packages? Because you shouldn't have to install MongoDB code if you're only using PostgreSQL. By keeping packages modular, your application remains lightweight and fast. Choose only the adapters and modules that you need for your specific technology stack.
1. Core & Workflows
What is it? These packages form the brain of the multi-tenant system. They handle the creation of tenants, grouping tenants together, and managing the security permissions.
When to use: You will always need tenant-core. You only need the others if your app has complex organizational hierarchies (like a parent company with multiple sub-companies).
@node-tenant/tenant-core
The mandatory core engine. It manages the context memory and permissions.
2. ORM Scoping Adapters
What is it? An ORM (Object-Relational Mapper) like Prisma or TypeORM helps you write database queries using JavaScript instead of raw SQL. These adapters intercept your JavaScript queries and automatically attach the active customer's ID to them.
Why do we need it? To prevent developers from accidentally forgetting to add WHERE tenantId = ... and leaking data.
@node-tenant/tenant-mongoose
Global query scoping plugin for Mongoose models.
3. Storage Adapters (Control Layer)
What is it? Your system needs a place to save the list of all your customers (tenants), their custom domain names, and their status (active/suspended). This is called the "Control Layer". These adapters tell the framework exactly which database technology to use for storing this list.
@node-tenant/tenant-storage-prisma
Store tenant metadata using Prisma.
@node-tenant/tenant-storage-mongodb
Store tenant metadata in MongoDB collections.
4. Database Adapters (Resource Provisioning)
What is it? If you are using advanced isolation strategies (like giving every customer their own physical database), the framework needs to know how to actually create those databases on your server. These adapters send raw SQL or MongoDB commands to create, delete, and migrate these physical databases.
@node-tenant/tenant-postgres
Executes Postgres `CREATE SCHEMA` commands.
@node-tenant/tenant-mongodb
Manages MongoDB connection switching.
5. Framework Adapters (Context Middlewares)
What is it? Your Node.js server needs to catch incoming HTTP requests, look at the URL or headers, figure out which customer is visiting, and pass that information to the Core Engine. These packages provide ready-made "middlewares" (interceptors) for your specific server framework.
@node-tenant/tenant-express
Express.js middlewares and context bindings.
Core Concepts
Understanding the core elements of @node-tenant/* helps you build robust architectures. The platform splits tenant resolution (figuring out who the customer is), lifecycle orchestration (creating/deleting customers), and data isolation (keeping data secure) into completely separate modules.
1. TenantManager
What is it? The TenantManager is the "brain" of your multi-tenant system. It is the central object that knows how to create, update, retrieve, and delete tenants.
Why do we need it? Instead of writing manual database queries to find a tenant's configuration, you ask the TenantManager. It wraps your storage adapters and triggers automated events (like provisioning a new database schema when a new tenant signs up).
import { TenantManager } from '@node-tenant/tenant-core';
import { createPrismaStorage } from '@node-tenant/tenant-storage-prisma';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const storage = createPrismaStorage(prisma);
// We initialize the manager globally once.
const manager = new TenantManager({
storage,
auditEnabled: true // Logs every tenant creation, update, and deletion securely.
});
// Create a new tenant dynamically
const newTenant = await manager.createTenant({
name: 'Acme Corp',
slug: 'acme',
strategy: 'tenantId', // They will share a database
status: 'active',
config: { plan: 'premium' } // Custom JSON data
});
Code Breakdown
createPrismaStoragetells the manager exactly how to save tenant metadata. Here we use Prisma.manager.createTenanthandles the entire lifecycle. If this tenant required a physical database (strategy: 'database'), this function would automatically tell your SQL server to create a new database.
2. TenantContext & AsyncLocalStorage
What is it? A hidden "backpack" of memory that Node.js carries around during a single HTTP request. We put the active tenant inside this backpack.
Why do we need it? Without this, you would have to pass a tenantId variable down through 15 layers of functions just to save a database record. With AsyncLocalStorage, the tenant ID is globally available anywhere in your code, but completely isolated from other simultaneous requests.
import { TenantContextManager } from '@node-tenant/tenant-core';
// Retrieve the currently active tenant context anywhere in your app:
const context = TenantContextManager.current();
console.log(context.slug); // e.g., 'acme'
// Or intentionally run a block of code AS a specific tenant:
await manager.runAsTenant('acme', async () => {
const currentCtx = TenantContextManager.currentOrThrow();
// All database queries or business logic inside this callback
// automatically run under the 'acme' context.
});
Common Mistake to Avoid
Beginners often try to save the active tenant in a global variable (e.g., global.activeTenant = "acme"). Never do this! Node.js handles multiple requests simultaneously. If you use a standard global variable, Customer B might overwrite Customer A's variable while their request is processing, causing Customer B to see Customer A's private data. Always use TenantContextManager which uses safe AsyncLocalStorage.
3. Events System
What is it? An event emitter. You can listen for things happening in your system (like a tenant being created) and trigger custom code.
Where is it used? Useful for sending welcome emails automatically when a tenant is created, or firing a webhook to an external billing provider (like Stripe).
// Subscribe to the 'tenant.created' event
manager.on('tenant.created', async (tenant) => {
console.log(`Tenant ${tenant.name} was successfully created!`);
// Example: await stripe.customers.create({ email: tenant.config.email });
});
// Subscribe to context switches (useful for advanced debugging)
manager.on('tenant.switched', ({ from, to }) => {
console.log(`Context switched from ${from?.slug} to ${to.slug}`);
});
Multi-Tenancy Strategies
What is it? A "strategy" defines how data is physically stored in your database.
Why do we need it? Different businesses have different security requirements. A small startup might want to put all customers in a single database to save money. A healthcare app might be legally required to put every customer in their own separate database.
tenantId — Shared Database
How it works: All tenants share the exact same database tables. Isolation is achieved logically by adding a tenant_id column to every single table, and automatically filtering every query (e.g., WHERE tenant_id = 'acme').
Example Database Table Setup
| ID | tenant_id | Name | |
|---|---|---|---|
| 1 | tenant_acme | Alice | alice@acme.com |
| 2 | tenant_globex | Bob | bob@globex.com |
import { TenantManager } from '@node-tenant/tenant-core';
// 1. Configure the manager to default to 'tenantId'
const manager = new TenantManager({
storage,
defaultStrategy: 'tenantId'
});
// 2. Create the tenant
await manager.createTenant({
name: 'Acme Corp',
slug: 'acme',
strategy: 'tenantId'
});
// Result: No new databases or schemas are created.
// Instead, subsequent ORM queries will automatically inject:
// WHERE tenant_id = 'acme'
Pros and Cons
- Pros: Very cheap to host. Extremely easy to run database migrations (you only run them once).
- Cons: "Noisy neighbor" problem (if one customer runs a huge query, it slows down the database for everyone). Highest risk of accidental data leaks if you forget a WHERE clause.
ORM Scoping & Adapters
Our query scoping adapters intercept database calls to filter queries by tenant ID or adjust search schemas automatically.
Prisma Scoping
Using useTenantPrisma(prisma) wraps the client in a proxy that automatically attaches filters for the tenantId strategy or sets the database schema for the schema strategy.
import { PrismaClient } from '@prisma/client';
import { useTenantPrisma } from '@node-tenant/tenant-prisma';
const prisma = new PrismaClient();
// In your route handler or controller:
const db = useTenantPrisma(prisma);
// Will only fetch users belonging to the active tenant context
const users = await db.user.findMany();
// Automatically links the created user to the active tenant
const newUser = await db.user.create({
data: { name: 'Sophia', email: 'sophia@example.com' }
});
Framework Integration
Automatically resolve the active tenant from request headers, subdomains, query parameters, or JWT payloads and run the request handlers in context.
Express Integration
import express from 'express';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, headerResolver } from '@node-tenant/tenant-express';
const app = express();
const manager = new TenantManager({ storage });
// Scopes request contexts using the 'x-tenant-id' request header
app.use(tenantMiddleware({
manager,
resolver: headerResolver('x-tenant-id')
}));
Next.js Integration
Use the Next.js middleware to resolve the active tenant before routing requests or executing API handlers.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { withTenant, subdomainResolver } from '@node-tenant/tenant-next';
export default withTenant({
resolver: subdomainResolver(),
onSuccess: (req, context) => {
return NextResponse.next();
}
});
Advanced Multi-Tenant Workflows
Go beyond simple database isolation. Support parent organizations, tenant-to-tenant collaborations, and direct item sharing.
1. Organizations
Group multiple tenants under a single parent organization (e.g. Hireflow Corp having tenants for USA, UK, and India).
import { OrganizationManager } from '@node-tenant/tenant-organizations';
const orgManager = new OrganizationManager(manager);
// Create organization with an initial tenant
const { organization, tenant } = await orgManager.createWithTenant({
orgName: 'Hireflow Corp',
orgSlug: 'hireflow',
tenantName: 'Hireflow USA',
tenantSlug: 'hireflow-usa',
strategy: 'tenantId'
});
2. Relationships
Enable partnerships or collaborations between independent tenants, letting them bridge workflows.
import { TenantRelationshipManager } from '@node-tenant/tenant-relations';
const relManager = new TenantRelationshipManager(manager);
// Establish a bidirectional partnership relationship
await relManager.createBidirectional(tenantAId, tenantBId, 'partner', {
purpose: 'Shared candidate pipeline'
});
// Check if tenantA is linked to tenantB as a partner
const isLinked = await relManager.hasRelationship(tenantAId, tenantBId, 'partner');
3. Resource Sharing
Share records (e.g. candidate profiles, job postings) securely across tenants with read or write permissions.
// Share a resource (e.g. a candidate profile)
await manager.shareResource({
resourceType: 'Candidate',
resourceId: 'cand_93019',
ownerTenantId: tenantAId,
targetTenantId: tenantBId,
permission: 'read'
});
Tenant Workflows & Connections
Learn how the request lifecycle flows in context, how to bridge isolated tenants together, and when to use different tenant configurations.
1. The Request Lifecycle Workflow
When an HTTP request is made to your multi-tenant backend, the platform processes it in 5 sequential steps:
Tenant Detection
The HTTP middleware inspects incoming indicators (subdomains, custom headers, query params, or JWT metadata) to identify the requesting tenant's slug.
Context Initialization
The resolved tenant's details (ID, slug, database strategy, and config) are bound to a thread-safe execution context using Node.js's AsyncLocalStorage.
Database Connection Routing
For schema or database strategies, the database adapter switches connection pools or changes schema search paths automatically before query execution.
Query Auto-Scoping
The ORM layer (e.g. Prisma proxy or Mongoose plugin) intercepts query calls to attach tenant filtering (e.g., automatically appending WHERE tenant_id = current_tenant).
Response & Teardown
The request completes, sends data back to the client, and the context automatically cleanses itself to prevent data leaking into subsequent calls.
2. Inter-Tenant Connections (Setup & Sharing)
In many enterprise applications, tenants need to collaborate. You can connect tenants by defining relationships and configuring sharing policies:
import { TenantRelationshipManager } from '@node-tenant/tenant-relations';
import { TenantManager } from '@node-tenant/tenant-core';
const manager = new TenantManager({ storage });
const relManager = new TenantRelationshipManager(manager);
// 1. Connect tenants during onboarding or setup:
await relManager.createBidirectional(
'tenant-id-acme-usa',
'tenant-id-acme-uk',
'collaboration',
{ connectedAt: new Date() }
);
// 2. Share a specific record (e.g., a candidate profile) across connected tenants:
const share = await manager.shareResource({
resourceType: 'Candidate',
resourceId: 'cand_84920',
ownerTenantId: 'tenant-id-acme-usa',
targetTenantId: 'tenant-id-acme-uk',
permission: 'read'
});
// 3. The target tenant can query shared resources dynamically:
const sharedShares = await manager.getShares({
targetTenantId: 'tenant-id-acme-uk',
resourceType: 'Candidate'
});
3. Concrete Use Cases of Tenants
B2B SaaS Portals
Each corporate client acts as an isolated tenant. Data is locked down entirely so employee data, messaging, and projects are strictly inaccessible to other companies.
Franchises & Regions
A brand has local stores or regional hubs (e.g., Europe, USA). Each store acts as a tenant to manage local inventory and staff independently, grouped under a parent organization.
Supply Chains
Vendors, logistics companies, and resellers run as tenants. They form relationships (partner connections) to exchange invoices, order logs, or tracking statuses.
Clinical Networks
Individual hospitals or clinics function as isolated tenants. Patient records are fully secure, but clinics can build a peer connection to transfer patient records during an emergency.