Skip to main content

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

  1. Database-level filtering: All queries automatically include organization_id
  2. Middleware enforcement: Tenant context required for all operations
  3. Service-level validation: Double-check organization access in services
  4. 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

  1. Partitioning: Consider table partitioning by organization for large datasets
  2. Indexing strategy: Composite indexes starting with organization_id
  3. Connection pooling: Per-tenant connection pools for high-volume orgs
  4. 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

  1. Always include organization_id in new tables
  2. Implement tenant filtering in all services
  3. Add feature gates for subscription-limited features
  4. 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
});
});