Multi-Tenant Architecture
The Stayzr Hotel Management System implements a sophisticated multi-tenant architecture that provides complete data isolation for guest communications while maintaining operational efficiency and cost-effectiveness.
🎯 Multi-Tenancy Model
Shared Infrastructure, Isolated Data
We use a shared database, shared schema approach with row-level security for tenant isolation:
- Single database instance serves all hotel tenants
- Shared application code with tenant-aware filtering
- Hotel-based isolation ensures guest data privacy and communication security
- Subscription-based feature gating per hotel property
🏗️ Architecture Components
🔒 Tenant Isolation Implementation
1. Database Schema Design
Organization as Tenant Root
-- Organizations table serves as the tenant root
CREATE TABLE organizations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
tenant_id VARCHAR(100) UNIQUE NOT NULL,
subscription_tier VARCHAR(50) NOT NULL DEFAULT 'starter',
subscription_status VARCHAR(50) NOT NULL DEFAULT 'active',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- All tenant data tables reference organization_id
CREATE TABLE users (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL REFERENCES organizations(id),
email VARCHAR(255) NOT NULL,
-- ... other fields
UNIQUE(organization_id, email)
);
CREATE TABLE guests (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL REFERENCES organizations(id),
email VARCHAR(255) NOT NULL,
phone VARCHAR(255),
-- ... other fields
UNIQUE(organization_id, email)
);
CREATE TABLE conversations (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL REFERENCES organizations(id),
guest_id INTEGER NOT NULL REFERENCES guests(id),
channel VARCHAR(50) NOT NULL, -- whatsapp, sms, email
status VARCHAR(50) NOT NULL DEFAULT 'active',
-- ... other fields
);
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL REFERENCES organizations(id),
conversation_id INTEGER NOT NULL REFERENCES conversations(id),
sender_type VARCHAR(50) NOT NULL, -- guest, staff, ai
content TEXT NOT NULL,
-- ... other fields
);
Tenant Isolation Indexes
-- Ensure all queries include organization_id for performance
CREATE INDEX idx_users_org_id ON users(organization_id);
CREATE INDEX idx_guests_org_id ON guests(organization_id);
CREATE INDEX idx_conversations_org_id ON conversations(organization_id);
CREATE INDEX idx_messages_org_id ON messages(organization_id);
CREATE INDEX idx_messages_conversation_id ON messages(conversation_id);
2. Middleware Stack
Tenant Isolation Middleware
export const tenantIsolationMiddleware = async (
req: TenantRequest,
res: Response,
next: NextFunction
) => {
try {
if (!req.user) {
return next(createError(401, "Authentication required"));
}
// Get user's organization context
const user = await prisma.user.findUnique({
where: { id: req.user.id },
include: { organization: true }
});
// Determine effective organization ID
let effectiveOrgId = user.organizationId;
// Handle super admin organization switching
if (user.isSuperAdmin && user.currentOrgId) {
effectiveOrgId = user.currentOrgId;
}
// Set tenant context for all subsequent operations
req.tenant = {
organizationId: effectiveOrgId,
tenantId: user.organization.tenantId,
isSuperAdmin: user.isSuperAdmin
};
next();
} catch (error) {
next(createError(500, "Tenant isolation failed"));
}
};
3. Data Access Patterns
Automatic Tenant Filtering
export abstract class BaseAuthenticatedService<T, CreateDTO, UpdateDTO> {
protected abstract filterByOrganization(userId: number): any;
async findMany(options?: IPaginationOptions, userId?: number): Promise<T[]> {
const where = {
...this.filterByOrganization(userId),
...options?.filters
};
return this.repository.findManyWithPagination(where, options);
}
}
// Implementation in specific services
export class GuestService extends BaseAuthenticatedService<Guest, CreateGuestDto, UpdateGuestDto> {
protected filterByOrganization(userId: number): any {
return {
organizationId: this.getCurrentOrganization(userId)
};
}
}
export class ConversationService extends BaseAuthenticatedService<Conversation, CreateConversationDto, UpdateConversationDto> {
protected filterByOrganization(userId: number): any {
return {
organizationId: this.getCurrentOrganization(userId)
};
}
}
👥 User Management & Organization Context
User Types and Access Levels
1. Super Admin
- Global access across all organizations
- Organization switching capability
- Platform management permissions
- Billing and subscription management
interface SuperAdminContext {
isSuperAdmin: true;
currentOrgId?: number; // Can switch organizations
globalPermissions: string[];
}
2. Organization Admin
- Full access within their organization
- User management for their organization
- Organization settings control
- Subscription management for their org
interface OrgAdminContext {
organizationId: number;
isOrgAdmin: true;
permissions: string[];
}
3. Regular Users
- Feature-based access within their organization
- Role-based permissions as assigned
- Data access based on ownership and sharing rules
interface UserContext {
organizationId: number;
roles: string[];
permissions: string[];
dataScope: 'own' | 'team' | 'department' | 'all';
}
Organization Switching Flow
🎛️ Feature Gating & Subscription Management
Subscription Tiers
interface SubscriptionTier {
name: 'starter' | 'professional' | 'enterprise';
limits: {
maxUsers: number | null;
maxGuests: number | null;
maxMessagesPerMonth: number | null;
maxActiveConversations: number | null;
maxStorageGB: number | null;
};
features: {
whatsappIntegration: boolean;
smsIntegration: boolean;
emailIntegration: boolean;
aiAutomation: boolean;
workflowAutomation: boolean;
advancedAnalytics: boolean;
customBranding: boolean;
apiAccess: boolean;
multiPropertyManagement: boolean;
};
}
Feature Gate Middleware
export const featureGateMiddleware = (requiredFeature: string) => {
return async (req: TenantRequest, res: Response, next: NextFunction) => {
const organization = await getOrganizationWithSubscription(
req.tenant.organizationId
);
if (!organization.subscriptionTier.features[requiredFeature]) {
return next(createError(403, `Feature '${requiredFeature}' not available in your subscription`));
}
next();
};
};
Usage Enforcement
export class SubscriptionEnforcementService {
async checkUsageLimit(
organizationId: number,
resource: string,
increment: number = 1
): Promise<boolean> {
const org = await this.getOrganizationWithLimits(organizationId);
const currentUsage = await this.getCurrentUsage(organizationId, resource);
const limit = org.subscriptionTier.limits[resource];
if (limit && (currentUsage + increment) > limit) {
throw createError(403, `${resource} limit exceeded for your subscription`);
}
return true;
}
async incrementUsage(
organizationId: number,
resource: string,
amount: number = 1
): Promise<void> {
await prisma.featureUsage.upsert({
where: {
organizationId_featureCode: {
organizationId,
featureCode: resource
}
},
update: {
currentUsage: { increment: amount }
},
create: {
organizationId,
featureCode: resource,
currentUsage: amount
}
});
}
}
🔐 Security Considerations
Data Isolation Guarantees
- Database-level filtering: All queries automatically include organization_id
- Middleware enforcement: Tenant context required for all operations
- Service-level validation: Double-check organization access in services
- Audit logging: Track all cross-organization access attempts
Super Admin Safeguards
export const superAdminMiddleware = async (
req: TenantRequest,
res: Response,
next: NextFunction
) => {
if (!req.tenant?.isSuperAdmin) {
return next(createError(403, "Super admin access required"));
}
// Log super admin actions for audit
await auditLogger.log({
userId: req.user.id,
action: 'super_admin_access',
resource: req.path,
organizationId: req.tenant.organizationId
});
next();
};
Cross-Tenant Access Prevention
// Automatic organization validation in all services
export abstract class BaseAuthenticatedService {
protected async validateOrganizationAccess(
resourceOrgId: number,
userOrgId: number,
isSuperAdmin: boolean = false
): Promise<void> {
if (!isSuperAdmin && resourceOrgId !== userOrgId) {
throw createError(403, "Access denied: organization mismatch");
}
}
}
📊 Performance Optimizations
Database Optimizations
- Partitioning: Consider table partitioning by organization for large datasets
- Indexing strategy: Composite indexes starting with organization_id
- Connection pooling: Per-tenant connection pools for high-volume orgs
- Query optimization: Ensure all queries include organization filters
Caching Strategy
// Organization-scoped caching
export class TenantAwareCache {
private getKey(organizationId: number, key: string): string {
return `org:${organizationId}:${key}`;
}
async get(organizationId: number, key: string): Promise<any> {
return redis.get(this.getKey(organizationId, key));
}
async set(organizationId: number, key: string, value: any, ttl?: number): Promise<void> {
const fullKey = this.getKey(organizationId, key);
if (ttl) {
await redis.setex(fullKey, ttl, JSON.stringify(value));
} else {
await redis.set(fullKey, JSON.stringify(value));
}
}
}
🚀 Scaling Considerations
Horizontal Scaling
- Stateless application servers enable easy horizontal scaling
- Database read replicas for read-heavy workloads
- Tenant-aware load balancing for high-volume organizations
Tenant Migration
- Hot migration capabilities for moving large tenants
- Backup and restore procedures per organization
- Data export/import tools for tenant onboarding/offboarding
Monitoring & Alerting
- Per-tenant metrics for usage monitoring
- Subscription limit alerting before limits are reached
- Performance monitoring by organization size and activity
🔧 Development Guidelines
Adding New Features
- Always include organization_id in new tables
- Implement tenant filtering in all services
- Add feature gates for subscription-limited features
- Test cross-tenant isolation in all new endpoints
Testing Multi-Tenancy
describe('Multi-tenant isolation', () => {
it('should not allow access to other organization data', async () => {
const org1User = await createTestUser({ organizationId: 1 });
const org2Guest = await createTestGuest({ organizationId: 2 });
const response = await request(app)
.get(`/guests/${org2Guest.id}`)
.set('Authorization', `Bearer ${org1User.token}`);
expect(response.status).toBe(404); // Should not find cross-org data
});
});