@venturialstd/tenant
v0.0.2
Published
Multi-tenant SaaS Platform Module for Venturial
Keywords
Readme
@venturialstd/tenant
A comprehensive Multi-Tenant SaaS Platform Package, developed by Venturial, that provides complete tenant management capabilities with user isolation for building scalable SaaS applications. This package follows the Venturial architecture pattern and integrates seamlessly with TypeORM and NestJS.
Features
- Multi-Tenant Management: Create and manage multiple tenants with isolated data
- User-Tenant Relationships: Complete user isolation per tenant with role-based access control
- Role-Based Access Control (RBAC): Owner, Admin, Member, and Viewer roles
- Subscription Plans: Support for Free, Starter, Professional, and Enterprise plans
- Trial Period: Configurable trial periods for new tenants
- Custom Domains: Optional custom domain support for tenants
- Invitation System: Invite users to tenants with pending invitation management
- Ownership Transfer: Transfer tenant ownership between users
- User Suspension: Suspend and reactivate user access to tenants
- Guards & Decorators: Built-in guards and decorators for easy tenant isolation
- CRUD Operations: Full TypeORM CRUD service integration
- Settings Management: Dynamic tenant-level settings with
@venturialstd/core
Installation
npm install @venturialstd/tenant
# or
yarn add @venturialstd/tenantBasic Usage
1. Import the TenantModule in your application
import { Module } from '@nestjs/common';
import { TenantModule } from '@venturialstd/tenant';
@Module({
imports: [
TenantModule,
// ... other modules
],
})
export class AppModule {}2. Use the services in your application
import { Injectable } from '@nestjs/common';
import { TenantService, TenantUserService, TenantUserRole } from '@venturialstd/tenant';
@Injectable()
export class YourService {
constructor(
private readonly tenantService: TenantService,
private readonly tenantUserService: TenantUserService,
) {}
async createNewTenant(userId: string) {
// Create tenant
const tenant = await this.tenantService.createTenant(
'Acme Corp',
'acme-corp',
'acme.example.com',
'A leading software company',
userId
);
// Add user as primary owner
await this.tenantUserService.addUserToTenant(
tenant.id,
userId,
TenantUserRole.OWNER
);
await this.tenantUserService.setPrimaryOwner(tenant.id, userId);
return tenant;
}
async inviteUserToTenant(tenantId: string, userId: string, invitedBy: string) {
return this.tenantUserService.inviteUserToTenant(
tenantId,
userId,
TenantUserRole.MEMBER,
invitedBy
);
}
}3. Use Guards and Decorators for tenant isolation
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
TenantGuard,
TenantRoleGuard,
TenantId,
UserId,
Roles,
TenantUserRole
} from '@venturialstd/tenant';
@Controller('data')
@UseGuards(TenantGuard) // Ensure user has access to tenant
export class DataController {
constructor(private readonly dataService: DataService) {}
@Get()
async getData(@TenantId() tenantId: string, @UserId() userId: string) {
// tenantId and userId are automatically extracted and validated
return this.dataService.getTenantData(tenantId, userId);
}
@Delete(':id')
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN) // Only owners and admins
@UseGuards(TenantRoleGuard) // Enforce role-based access
async deleteData(@TenantId() tenantId: string, @Param('id') id: string) {
return this.dataService.deleteData(tenantId, id);
}
}Entity Structures
Tenant Entity
The Tenant entity includes the following fields:
- id: UUID primary key
- name: Tenant name (required, unique)
- slug: URL-friendly identifier (required, unique)
- domain: Custom domain (optional)
- description: Tenant description (optional)
- isActive: Active/inactive status (default: true)
- settings: JSON field for tenant-specific settings
- ownerId: Reference to the owner user
- plan: Subscription plan (free, starter, professional, enterprise)
- trialEndsAt: Trial period expiration date
- subscriptionEndsAt: Subscription expiration date
- createdAt: Creation timestamp
- updatedAt: Last update timestamp
TenantUser Entity
The TenantUser entity manages user-tenant relationships:
- id: UUID primary key
- tenantId: Reference to tenant (indexed)
- userId: Reference to user (indexed)
- role: User role (OWNER, ADMIN, MEMBER, VIEWER)
- status: User status (ACTIVE, INVITED, SUSPENDED)
- isPrimary: Whether user is the primary owner
- invitedBy: User who sent the invitation
- invitedAt: Invitation timestamp
- joinedAt: Join timestamp
- permissions: JSON field for custom permissions
- createdAt: Creation timestamp
- updatedAt: Last update timestamp
Unique constraint: (tenantId, userId) - A user can only be added once per tenant
Available Methods
TenantService Methods
createTenant(name, slug, domain?, description?, ownerId?)
Creates a new tenant with validation and default settings.
const tenant = await tenantService.createTenant(
'My Company',
'my-company',
'mycompany.com',
'Company description',
'owner-uuid'
);getTenantBySlug(slug)
Retrieve a tenant by its slug.
const tenant = await tenantService.getTenantBySlug('my-company');getTenantByDomain(domain)
Retrieve a tenant by its custom domain.
const tenant = await tenantService.getTenantByDomain('mycompany.com');updateTenantSettings(tenantId, settings)
Update tenant-specific settings.
await tenantService.updateTenantSettings('tenant-uuid', {
theme: 'dark',
language: 'en',
features: { api: true }
});setTenantStatus(tenantId, isActive)
Activate or deactivate a tenant.
await tenantService.setTenantStatus('tenant-uuid', false); // Deactivate
await tenantService.setTenantStatus('tenant-uuid', true); // ActivateupdateTenantPlan(tenantId, plan, subscriptionEndsAt?)
Update the tenant's subscription plan.
const endDate = new Date();
endDate.setMonth(endDate.getMonth() + 1);
await tenantService.updateTenantPlan(
'tenant-uuid',
'professional',
endDate
);getActiveTenants()
Get all active tenants.
const activeTenants = await tenantService.getActiveTenants();getTenantsByOwner(ownerId)
Get all tenants owned by a specific user.
const tenants = await tenantService.getTenantsByOwner('owner-uuid');isInTrialPeriod(tenantId)
Check if a tenant is currently in trial period.
const inTrial = await tenantService.isInTrialPeriod('tenant-uuid');hasActiveSubscription(tenantId)
Check if a tenant has an active subscription.
const isActive = await tenantService.hasActiveSubscription('tenant-uuid');TenantUserService Methods
addUserToTenant(tenantId, userId, role, invitedBy?)
Add a user to a tenant with a specific role.
await tenantUserService.addUserToTenant(
'tenant-uuid',
'user-uuid',
TenantUserRole.MEMBER,
'inviter-uuid'
);inviteUserToTenant(tenantId, userId, role, invitedBy)
Invite a user to a tenant (creates pending invitation).
await tenantUserService.inviteUserToTenant(
'tenant-uuid',
'user-uuid',
TenantUserRole.ADMIN,
'inviter-uuid'
);acceptInvitation(tenantId, userId)
Accept a pending tenant invitation.
await tenantUserService.acceptInvitation('tenant-uuid', 'user-uuid');removeUserFromTenant(tenantId, userId)
Remove a user from a tenant (cannot remove primary owner).
await tenantUserService.removeUserFromTenant('tenant-uuid', 'user-uuid');updateUserRole(tenantId, userId, newRole)
Update a user's role in a tenant.
await tenantUserService.updateUserRole(
'tenant-uuid',
'user-uuid',
TenantUserRole.ADMIN
);getUserTenants(userId)
Get all tenants a user belongs to.
const userTenants = await tenantUserService.getUserTenants('user-uuid');getTenantUsers(tenantId)
Get all users in a tenant (including invited).
const users = await tenantUserService.getTenantUsers('tenant-uuid');getActiveTenantUsers(tenantId)
Get all active users in a tenant.
const activeUsers = await tenantUserService.getActiveTenantUsers('tenant-uuid');hasAccess(tenantId, userId)
Check if a user has access to a tenant.
const hasAccess = await tenantUserService.hasAccess('tenant-uuid', 'user-uuid');hasRole(tenantId, userId, role)
Check if a user has a specific role in a tenant.
const isAdmin = await tenantUserService.hasRole(
'tenant-uuid',
'user-uuid',
TenantUserRole.ADMIN
);isAdminOrOwner(tenantId, userId)
Check if a user is an admin or owner in a tenant.
const isAdminOrOwner = await tenantUserService.isAdminOrOwner('tenant-uuid', 'user-uuid');getUserRole(tenantId, userId)
Get a user's role in a tenant.
const role = await tenantUserService.getUserRole('tenant-uuid', 'user-uuid');setPrimaryOwner(tenantId, userId)
Set a user as the primary owner of a tenant.
await tenantUserService.setPrimaryOwner('tenant-uuid', 'user-uuid');transferOwnership(tenantId, fromUserId, toUserId)
Transfer tenant ownership between users.
await tenantUserService.transferOwnership(
'tenant-uuid',
'current-owner-uuid',
'new-owner-uuid'
);getUserInvitations(userId)
Get all pending invitations for a user.
const invitations = await tenantUserService.getUserInvitations('user-uuid');suspendUser(tenantId, userId)
Suspend a user's access to a tenant.
await tenantUserService.suspendUser('tenant-uuid', 'user-uuid');reactivateUser(tenantId, userId)
Reactivate a suspended user.
await tenantUserService.reactivateUser('tenant-uuid', 'user-uuid');Guards and Decorators
TenantGuard
Enforces tenant access control by validating that the authenticated user has access to the requested tenant.
@Controller('api')
@UseGuards(TenantGuard)
export class ApiController {
@Get('data')
async getData(@TenantId() tenantId: string) {
// User access to tenant is already validated
return this.service.getData(tenantId);
}
}TenantRoleGuard
Enforces role-based access control within a tenant. Use with @Roles() decorator.
@Controller('admin')
@UseGuards(TenantGuard, TenantRoleGuard)
export class AdminController {
@Delete('user/:userId')
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
async removeUser(@TenantId() tenantId: string, @Param('userId') userId: string) {
// Only owners and admins can access this endpoint
return this.service.removeUser(tenantId, userId);
}
}@TenantId()
Extracts the tenant ID from the request. Checks in order:
request.tenantIdrequest.headers['x-tenant-id']request.params.tenantIdrequest.query.tenantId
@Get()
async getData(@TenantId() tenantId: string) {
return this.service.getData(tenantId);
}@UserId()
Extracts the user ID from the authenticated request.
@Get('profile')
async getProfile(@UserId() userId: string, @TenantId() tenantId: string) {
return this.service.getUserProfile(tenantId, userId);
}@TenantContext()
Extracts the complete tenant context including tenantId, userId, role, and user object.
@Get()
async getData(@TenantContext() context: { tenantId: string, userId: string, role: TenantUserRole, user: any }) {
return this.service.getData(context);
}@Roles(...roles)
Specifies required roles for accessing an endpoint. Must be used with TenantRoleGuard.
@Post('invite')
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
@UseGuards(TenantGuard, TenantRoleGuard)
async inviteUser(@TenantId() tenantId: string, @Body() dto: InviteUserDto) {
return this.service.inviteUser(tenantId, dto);
}Configuration Settings
The module provides the following configurable settings through SettingsService:
General Settings
- ENABLED: Enable/disable the tenant module (boolean)
- MAX_TENANTS: Maximum number of tenants allowed (0 = unlimited)
- DEFAULT_PLAN: Default subscription plan for new tenants
- TRIAL_DAYS: Number of trial days for new tenants (default: 14)
Feature Settings
- CUSTOM_DOMAIN: Allow custom domains for tenants
- API_ACCESS: Allow API access for tenants
Enums and Types
TenantUserRole
enum TenantUserRole {
OWNER = 'owner',
ADMIN = 'admin',
MEMBER = 'member',
VIEWER = 'viewer',
}TenantUserStatus
enum TenantUserStatus {
ACTIVE = 'active',
INVITED = 'invited',
SUSPENDED = 'suspended',
}TENANT_PLAN
enum TENANT_PLAN {
FREE = 'free',
STARTER = 'starter',
PROFESSIONAL = 'professional',
ENTERPRISE = 'enterprise',
}TENANT_STATUS
enum TENANT_STATUS {
ACTIVE = 'active',
INACTIVE = 'inactive',
SUSPENDED = 'suspended',
TRIAL = 'trial',
}Integration with @venturialstd/core
This package uses @venturialstd/core for:
- SharedModule: Core functionality and configuration
- SettingsService: Dynamic settings management
- AppLogger: Structured logging
TypeORM Integration
The package extends TypeOrmCrudService from @dataui/crud-typeorm, providing:
- Standard CRUD operations
- Query builders
- Pagination support
- Filtering and sorting capabilities
Example: Complete SaaS Implementation with User Isolation
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TenantModule, TenantService, TenantUserService } from '@venturialstd/tenant';
@Module({
imports: [
TypeOrmModule.forRoot({
// Your database configuration
}),
TenantModule,
],
})
export class AppModule {}
// Service for onboarding new tenants
@Injectable()
export class SaasService {
constructor(
private readonly tenantService: TenantService,
private readonly tenantUserService: TenantUserService,
) {}
async onboardNewTenant(data: any, ownerId: string) {
// Create tenant
const tenant = await this.tenantService.createTenant(
data.companyName,
data.slug,
data.domain,
data.description,
ownerId
);
// Add owner to tenant
await this.tenantUserService.addUserToTenant(
tenant.id,
ownerId,
TenantUserRole.OWNER
);
// Set as primary owner
await this.tenantUserService.setPrimaryOwner(tenant.id, ownerId);
// Configure tenant settings
await this.tenantService.updateTenantSettings(tenant.id, {
theme: data.preferences?.theme || 'light',
locale: data.preferences?.locale || 'en',
notifications: true,
});
return tenant;
}
async inviteTeamMember(
tenantId: string,
userId: string,
invitedBy: string,
role: TenantUserRole = TenantUserRole.MEMBER
) {
// Verify inviter is admin or owner
const isAdmin = await this.tenantUserService.isAdminOrOwner(tenantId, invitedBy);
if (!isAdmin) {
throw new ForbiddenException('Only admins can invite team members');
}
// Invite user
await this.tenantUserService.inviteUserToTenant(
tenantId,
userId,
role,
invitedBy
);
// Send invitation email (implement your email service)
// await this.emailService.sendInvitation(userId, tenantId);
}
async checkTenantAccess(tenantId: string, userId: string) {
const tenant = await this.tenantService.repo.findOne({
where: { id: tenantId }
});
if (!tenant.isActive) {
throw new UnauthorizedException('Tenant is inactive');
}
const hasAccess = await this.tenantUserService.hasAccess(tenantId, userId);
if (!hasAccess) {
throw new UnauthorizedException('No access to this tenant');
}
const hasSubscription = await this.tenantService.hasActiveSubscription(tenantId);
if (!hasSubscription) {
throw new UnauthorizedException('Subscription expired');
}
return true;
}
}
// Controller with tenant isolation
@Controller('projects')
@UseGuards(TenantGuard)
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
@Get()
async list(@TenantId() tenantId: string, @UserId() userId: string) {
// Automatically scoped to tenant
return this.projectsService.findByTenant(tenantId);
}
@Post()
async create(
@TenantId() tenantId: string,
@UserId() userId: string,
@Body() dto: CreateProjectDto
) {
return this.projectsService.create(tenantId, userId, dto);
}
@Delete(':id')
@Roles(TenantUserRole.OWNER, TenantUserRole.ADMIN)
@UseGuards(TenantRoleGuard)
async delete(@TenantId() tenantId: string, @Param('id') id: string) {
// Only owners and admins can delete
return this.projectsService.delete(tenantId, id);
}
}Database Migrations
The package requires two database tables: tenant and tenant_user. Create migrations for both:
Tenant Table Migration
npm run typeorm migration:create -- -n CreateTenantimport { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
export class CreateTenant1234567890123 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'tenant',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'name', type: 'varchar', isUnique: true },
{ name: 'slug', type: 'varchar', isUnique: true },
{ name: 'domain', type: 'varchar', isNullable: true },
{ name: 'description', type: 'text', isNullable: true },
{ name: 'isActive', type: 'boolean', default: true },
{ name: 'settings', type: 'jsonb', isNullable: true },
{ name: 'ownerId', type: 'varchar', isNullable: true },
{ name: 'plan', type: 'varchar', isNullable: true },
{ name: 'trialEndsAt', type: 'timestamptz', isNullable: true },
{ name: 'subscriptionEndsAt', type: 'timestamptz', isNullable: true },
{ name: 'createdAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
{ name: 'updatedAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
],
}),
true,
);
await queryRunner.createIndex('tenant', new TableIndex({ name: 'IDX_tenant_slug', columnNames: ['slug'] }));
await queryRunner.createIndex('tenant', new TableIndex({ name: 'IDX_tenant_domain', columnNames: ['domain'] }));
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('tenant');
}
}TenantUser Table Migration
npm run typeorm migration:create -- -n CreateTenantUserimport { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
export class CreateTenantUser1234567890124 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'tenant_user',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'tenantId', type: 'uuid' },
{ name: 'userId', type: 'uuid' },
{ name: 'role', type: 'enum', enum: ['owner', 'admin', 'member', 'viewer'], default: "'member'" },
{ name: 'status', type: 'enum', enum: ['active', 'invited', 'suspended'], default: "'active'" },
{ name: 'isPrimary', type: 'boolean', default: false },
{ name: 'invitedBy', type: 'uuid', isNullable: true },
{ name: 'invitedAt', type: 'timestamptz', isNullable: true },
{ name: 'joinedAt', type: 'timestamptz', isNullable: true },
{ name: 'permissions', type: 'jsonb', isNullable: true },
{ name: 'createdAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
{ name: 'updatedAt', type: 'timestamptz', default: 'CURRENT_TIMESTAMP' },
],
}),
true,
);
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_tenantId', columnNames: ['tenantId'] }));
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_userId', columnNames: ['userId'] }));
await queryRunner.createIndex('tenant_user', new TableIndex({ name: 'IDX_tenant_user_tenant_user', columnNames: ['tenantId', 'userId'], isUnique: true }));
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('tenant_user');
}
}Run migrations:
npm run typeorm migration:runTesting
The module includes an isolated NestJS test environment for testing all functionality without integrating it into your main application.
Setup Test Environment
Copy environment configuration:
cd test cp .env.example .envConfigure your database in
test/.env:DB_HOST=localhost DB_PORT=5432 DB_USERNAME=postgres DB_PASSWORD=postgres DB_DATABASE=tenant_test TEST_PORT=3001Install dependencies (from the module root):
npm installCreate test database:
createdb tenant_test # or using psql: psql -U postgres -c "CREATE DATABASE tenant_test;"
Run Test Server
Start the test server (runs on port 3001 by default):
npm run test:devOr with auto-reload on file changes:
npm run test:watchThe server will display all available endpoints:
🚀 Tenant Module Test Server running on: http://localhost:3001
📋 Available endpoints:
Tenant Management:
POST /tenants - Create tenant
GET /tenants - Get all tenants
GET /tenants/active - Get active tenants
GET /tenants/owner/:ownerId - Get tenants by owner
GET /tenants/slug/:slug - Get tenant by slug
GET /tenants/domain/:domain - Get tenant by domain
GET /tenants/:id - Get tenant by ID
PUT /tenants/:id/settings - Update tenant settings
PUT /tenants/:id/status - Update tenant status
PUT /tenants/:id/plan - Update tenant plan
GET /tenants/:id/trial - Check if trial is active
GET /tenants/:id/subscription - Check if subscription is active
DELETE /tenants/:id - Delete tenant
User-Tenant Management:
POST /tenant-users/add - Add user to tenant
POST /tenant-users/invite - Invite user to tenant
GET /tenant-users/tenant/:tenantId - Get all users in tenant
GET /tenant-users/user/:userId - Get user's tenants
PUT /tenant-users/update-role - Update user role
POST /tenant-users/transfer-ownership - Transfer ownership
... and moreTesting with HTTP Requests
Use the provided test/requests.http file with REST Client (VS Code extension) or Postman:
Create a tenant:
POST http://localhost:3001/tenants Content-Type: application/json { "name": "Acme Corporation", "slug": "acme-corp", "ownerId": "user-123", "domain": "acme.example.com" }Add users to tenant:
POST http://localhost:3001/tenant-users/add Content-Type: application/json { "tenantId": "<tenant-id>", "userId": "user-456", "role": "member" }Test role-based access:
GET http://localhost:3001/tenant-users/role/<tenant-id>/user-456
See test/requests.http for a complete set of example requests.
Test Structure
test/
├── .env.example # Environment configuration template
├── main.ts # Test server bootstrap
├── test-app.module.ts # Test NestJS application module
├── requests.http # HTTP request examples
└── controllers/
├── tenant-test.controller.ts # Tenant CRUD endpoints
└── tenant-user-test.controller.ts # User-tenant management endpointsDevelopment
Build the package
npm run buildPublish the package
npm run release:patchDependencies
@nestjs/common^11.0.11@nestjs/typeorm^10.0.0@venturialstd/core^1.0.16@dataui/crud-typeorm(for CRUD operations)typeorm^0.3.20class-validator^0.14.1class-transformer^0.5.1
License
This package is part of the Venturial ecosystem and follows the organization's licensing.
Support
For issues, questions, or contributions, please contact the Venturial development team.
