Multi Tenant Architecture - gabrielmaialva33/innkeeper GitHub Wiki

🏢 Multi-Tenant Architecture

Comprehensive guide to Innkeeper's multi-tenant architecture and implementation


📋 Overview

Innkeeper is built from the ground up as a multi-tenant SaaS application, allowing multiple organizations (hotels, hotel chains, property management companies) to use the same application instance while maintaining complete data isolation and customization. This document provides a detailed overview of the multi-tenant architecture, implementation strategies, and best practices.

Key Multi-Tenancy Features:

  • Complete Data Isolation - Each tenant's data is completely separated
  • Customizable Settings - Per-tenant configuration and business rules
  • Scalable Architecture - Single codebase serving multiple tenants
  • Security by Design - Row-level security and access controls
  • Performance Optimized - Efficient resource utilization across tenants

🎯 Multi-Tenancy Strategy

Architecture Pattern: Single Database, Multi-Tenant

Innkeeper uses a single database, multi-tenant approach with row-level security (RLS) for optimal cost-effectiveness and maintainability:

graph TB
    subgraph "Single Application Instance"
        APP[Innkeeper Application]
        MIDDLEWARE[Tenant Context Middleware]
    end
    
    subgraph "Tenant Identification"
        ORG_A[Organization A<br/>Hotel Chain]
        ORG_B[Organization B<br/>Boutique Hotel]
        ORG_C[Organization C<br/>Resort Group]
    end
    
    subgraph "Shared Database with RLS"
        DB[(PostgreSQL Database)]
        RLS[Row Level Security]
        ORGS[organizations table]
        HOTELS[hotels table]
        RESERVATIONS[reservations table]
        GUESTS[guests table]
    end
    
    ORG_A --> APP
    ORG_B --> APP
    ORG_C --> APP
    
    APP --> MIDDLEWARE
    MIDDLEWARE --> DB
    DB --> RLS
    RLS --> ORGS
    RLS --> HOTELS
    RLS --> RESERVATIONS
    RLS --> GUESTS
    
    ORGS -.-> HOTELS
    ORGS -.-> RESERVATIONS
    ORGS -.-> GUESTS
Loading

Why Single Database Multi-Tenancy?

✅ Advantages

  • Cost Efficiency: Single infrastructure for all tenants
  • Simplified Maintenance: One database to backup, update, and monitor
  • Resource Optimization: Better utilization of database connections and memory
  • Feature Consistency: All tenants get new features simultaneously
  • Easier Scaling: Horizontal scaling without complex data distribution

⚠️ Considerations

  • Careful Security Implementation: Requires robust row-level security
  • Performance Monitoring: Need to monitor for tenant-specific performance issues
  • Backup Complexity: Single backup contains all tenant data
  • Compliance Requirements: May need special handling for data residency

🏗️ Database Design for Multi-Tenancy

Tenant Entity: Organizations

The organizations table serves as the primary tenant entity:

CREATE TABLE organizations (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    tax_id VARCHAR(255),
    email VARCHAR(255) NOT NULL,
    phone VARCHAR(255),
    
    -- Address information
    address VARCHAR(255),
    city VARCHAR(255),
    state VARCHAR(255),
    country VARCHAR(255),
    postal_code VARCHAR(255),
    
    -- Business settings
    currency VARCHAR(3) DEFAULT 'USD',
    timezone VARCHAR(255) DEFAULT 'UTC',
    
    -- Status flags
    is_active BOOLEAN DEFAULT true,
    is_deleted BOOLEAN DEFAULT false,
    
    -- Tenant-specific configuration
    settings JSONB DEFAULT '{
        "overbooking_percentage": 10,
        "default_check_in_time": "14:00",
        "default_check_out_time": "11:00",
        "cancellation_policy": "flexible",
        "payment_methods": ["cash", "card"],
        "supported_languages": ["en", "pt"],
        "night_audit_time": "02:00"
    }',
    
    -- Additional metadata
    metadata JSONB DEFAULT '{}',
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_organizations_active ON organizations(is_active);
CREATE INDEX idx_organizations_deleted ON organizations(is_deleted);

Tenant Isolation Pattern

Every tenant-specific table includes an organization_id foreign key:

-- Example: Hotels table with tenant isolation
CREATE TABLE hotels (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) NOT NULL,
    -- ... other columns
    
    UNIQUE(organization_id, slug),  -- Unique within tenant
    INDEX(organization_id)          -- Performance index
);

-- Example: Reservations table with tenant isolation
CREATE TABLE reservations (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
    hotel_id INTEGER NOT NULL REFERENCES hotels(id),
    guest_id INTEGER NOT NULL REFERENCES guests(id),
    -- ... other columns
    
    INDEX(organization_id),
    INDEX(organization_id, check_in_date, check_out_date)
);

Multi-Tenant Data Model

erDiagram
    organizations ||--o{ hotels : "owns"
    organizations ||--o{ users : "employs"
    organizations ||--o{ guests : "manages"
    organizations ||--o{ staff : "employs"
    
    hotels ||--o{ rooms : "contains"
    hotels ||--o{ room_types : "defines"
    hotels ||--o{ reservations : "receives"
    hotels ||--o{ services : "offers"
    
    guests ||--o{ reservations : "makes"
    reservations ||--o{ payments : "generates"
    reservations ||--o{ folios : "creates"
    
    organizations {
        int id PK
        string name
        string slug UK
        string currency
        string timezone
        jsonb settings
        boolean is_active
    }
    
    hotels {
        int id PK
        int organization_id FK
        string name
        string slug
        string city
        int star_rating
    }
    
    reservations {
        int id PK
        int organization_id FK
        int hotel_id FK
        int guest_id FK
        date check_in_date
        date check_out_date
        string status
    }
Loading

🔒 Row-Level Security Implementation

PostgreSQL RLS Setup

Innkeeper uses PostgreSQL's Row-Level Security (RLS) for database-level tenant isolation:

-- Enable RLS on tenant-specific tables
ALTER TABLE hotels ENABLE ROW LEVEL SECURITY;
ALTER TABLE reservations ENABLE ROW LEVEL SECURITY;
ALTER TABLE guests ENABLE ROW LEVEL SECURITY;
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;

-- Create RLS policies for tenant isolation
CREATE POLICY tenant_isolation_policy ON hotels
    FOR ALL
    TO application_role
    USING (organization_id = current_setting('app.current_organization')::INTEGER);

CREATE POLICY tenant_isolation_policy ON reservations
    FOR ALL
    TO application_role
    USING (organization_id = current_setting('app.current_organization')::INTEGER);

-- Function to set current organization context
CREATE OR REPLACE FUNCTION set_current_organization(org_id INTEGER)
RETURNS VOID AS $$
BEGIN
    PERFORM set_config('app.current_organization', org_id::TEXT, false);
END;
$$ LANGUAGE plpgsql;

Application-Level Context Management

The TenantContextMiddleware manages tenant context for each request:

// app/middleware/tenant_context_middleware.ts
export default class TenantContextMiddleware {
  async handle({ auth, response }: HttpContext, next: NextFn) {
    try {
      // Get organization ID from authenticated user
      const organizationId = auth.user?.organization_id
      
      if (!organizationId) {
        // Handle users without organization (super admins)
        return await next()
      }
      
      // Set PostgreSQL session variable for RLS
      await db.rawQuery('SELECT set_current_organization(?)', [organizationId])
      
      // Continue with request - all queries now filtered by tenant
      return await next()
      
    } catch (error) {
      return response.status(500).json({
        error: 'Failed to set tenant context'
      })
    }
  }
}

🔧 Application-Level Tenant Handling

Model-Level Tenant Awareness

All tenant-specific models include organization relationships:

// app/models/hotel.ts
export default class Hotel extends BaseModel {
  @column()
  declare organization_id: number
  
  @belongsTo(() => Organization)
  declare organization: BelongsTo<typeof Organization>
  
  // Automatic tenant filtering in queries
  static boot() {
    super.boot()
    
    this.addGlobalScope('tenant', (query) => {
      const organizationId = getCurrentOrganizationId()
      if (organizationId) {
        query.where('organization_id', organizationId)
      }
    })
  }
}

Service-Level Tenant Context

Business logic services maintain tenant context:

// app/services/reservation_service.ts
export class ReservationService {
  constructor(private organizationId: number) {}
  
  async createReservation(data: CreateReservationData) {
    // Ensure all created records belong to current tenant
    const reservation = await Reservation.create({
      ...data,
      organization_id: this.organizationId
    })
    
    return reservation
  }
  
  async findAvailableRooms(hotelId: number, dates: DateRange) {
    // Queries automatically filtered by RLS
    return Room.query()
      .where('hotel_id', hotelId)
      .where('status', 'available')
      // organization_id filter applied by RLS
  }
}

Controller-Level Tenant Validation

Controllers validate tenant access:

// app/controllers/hotels_controller.ts
export default class HotelsController {
  async show({ params, auth, response }: HttpContext) {
    const hotel = await Hotel.find(params.id)
    
    if (!hotel) {
      return response.notFound({ message: 'Hotel not found' })
    }
    
    // Additional validation (RLS already filters, but explicit check for clarity)
    if (hotel.organization_id !== auth.user!.organization_id) {
      return response.forbidden({ message: 'Access denied' })
    }
    
    return hotel
  }
}

🔐 Security Considerations

Data Isolation Security

graph TB
    subgraph "Security Layers"
        APP_AUTH[Application Authentication]
        TENANT_CTX[Tenant Context Middleware]
        RLS[PostgreSQL Row Level Security]
        FK_CONSTRAINTS[Foreign Key Constraints]
    end
    
    subgraph "Request Flow"
        USER[User Request]
        AUTH_CHECK[Authentication Check]
        TENANT_SET[Set Tenant Context]
        QUERY[Database Query]
        FILTERED_DATA[Tenant-Filtered Data]
    end
    
    USER --> AUTH_CHECK
    AUTH_CHECK --> APP_AUTH
    APP_AUTH --> TENANT_SET
    TENANT_SET --> TENANT_CTX
    TENANT_CTX --> QUERY
    QUERY --> RLS
    RLS --> FK_CONSTRAINTS
    FK_CONSTRAINTS --> FILTERED_DATA
Loading

Security Best Practices

1. Defense in Depth

// Multiple layers of tenant validation
class SecureService {
  async getData(id: number, user: User) {
    // Layer 1: Application-level check
    if (!user.organization_id) {
      throw new UnauthorizedException('No organization access')
    }
    
    // Layer 2: Explicit tenant filtering
    const data = await Model.query()
      .where('id', id)
      .where('organization_id', user.organization_id)
      .first()
    
    // Layer 3: RLS provides additional protection
    return data
  }
}

2. Audit Logging

// Track all tenant-related operations
export class AuditService {
  async logTenantAccess(user: User, resource: string, action: string) {
    await AuditLog.create({
      user_id: user.id,
      organization_id: user.organization_id,
      resource,
      action,
      ip_address: request.ip(),
      user_agent: request.header('user-agent'),
      timestamp: DateTime.now()
    })
  }
}

3. Input Validation

// Validate tenant-specific constraints
export class TenantValidator {
  static validateHotelAccess = vine.compile(
    vine.object({
      hotel_id: vine.number().exists(async (db, value, field) => {
        const hotel = await db
          .from('hotels')
          .where('id', value)
          .where('organization_id', field.meta.organizationId)
          .first()
        
        return !!hotel
      })
    })
  )
}

📊 Performance Optimization

Database Performance

Indexing Strategy

-- Composite indexes for tenant + common query patterns
CREATE INDEX idx_reservations_org_dates ON reservations(organization_id, check_in_date, check_out_date);
CREATE INDEX idx_guests_org_email ON guests(organization_id, email);
CREATE INDEX idx_rooms_org_status ON rooms(organization_id, status);
CREATE INDEX idx_payments_org_created ON payments(organization_id, created_at);

-- Partial indexes for active records
CREATE INDEX idx_hotels_org_active ON hotels(organization_id) WHERE is_active = true;
CREATE INDEX idx_users_org_active ON users(organization_id) WHERE is_deleted = false;

Query Optimization

// Efficient tenant-aware queries
class OptimizedQueries {
  // Good: Uses composite index
  async getActiveReservations(organizationId: number) {
    return db.query()
      .from('reservations')
      .where('organization_id', organizationId)
      .where('status', 'active')
      .orderBy('check_in_date')
  }
  
  // Better: Leverages RLS (organization_id filter automatic)
  async getActiveReservationsWithRLS() {
    return Reservation.query()
      .where('status', 'active')
      .orderBy('check_in_date')
  }
}

Caching Strategy

Tenant-Aware Caching

// Cache keys include tenant context
export class TenantCache {
  private static getCacheKey(organizationId: number, key: string): string {
    return `org:${organizationId}:${key}`
  }
  
  static async get(organizationId: number, key: string) {
    const cacheKey = this.getCacheKey(organizationId, key)
    return await Redis.get(cacheKey)
  }
  
  static async set(organizationId: number, key: string, value: any, ttl = 3600) {
    const cacheKey = this.getCacheKey(organizationId, key)
    return await Redis.setex(cacheKey, ttl, JSON.stringify(value))
  }
}

🚀 Scaling Considerations

Horizontal Scaling

graph TB
    subgraph "Load Balancer"
        LB[Nginx Load Balancer]
    end
    
    subgraph "Application Tier"
        APP1[App Instance 1<br/>Tenants: A, B, C]
        APP2[App Instance 2<br/>Tenants: A, B, C]
        APP3[App Instance 3<br/>Tenants: A, B, C]
    end
    
    subgraph "Database Tier"
        MASTER[(Master DB<br/>All Tenants)]
        REPLICA1[(Read Replica 1)]
        REPLICA2[(Read Replica 2)]
    end
    
    subgraph "Cache Tier"
        REDIS1[(Redis Cluster 1)]
        REDIS2[(Redis Cluster 2)]
    end
    
    LB --> APP1
    LB --> APP2
    LB --> APP3
    
    APP1 --> MASTER
    APP2 --> REPLICA1
    APP3 --> REPLICA2
    
    APP1 --> REDIS1
    APP2 --> REDIS2
    APP3 --> REDIS1
Loading

Tenant Distribution Strategies

1. Even Distribution (Current)

  • All tenants share the same infrastructure
  • Simple and cost-effective
  • Good for similar-sized tenants

2. Tier-Based Distribution (Future)

// Route large tenants to dedicated resources
class TenantRouter {
  static getConnectionConfig(organizationId: number) {
    const tenant = TenantRegistry.get(organizationId)
    
    if (tenant.tier === 'enterprise') {
      return {
        database: 'enterprise_db',
        redis: 'enterprise_redis',
        queue: 'enterprise_queue'
      }
    }
    
    return {
      database: 'shared_db',
      redis: 'shared_redis',
      queue: 'shared_queue'
    }
  }
}

Migration Path to Microservices

graph LR
    subgraph "Current: Modular Monolith"
        MONOLITH[Innkeeper<br/>Multi-Tenant Monolith]
    end
    
    subgraph "Future: Tenant-Aware Microservices"
        AUTH_MS[Auth Service<br/>Cross-Tenant]
        HOTEL_MS[Hotel Service<br/>Tenant-Aware]
        RESERVATION_MS[Reservation Service<br/>Tenant-Aware]
        PAYMENT_MS[Payment Service<br/>Tenant-Aware]
    end
    
    subgraph "Service Mesh"
        MESH[Istio/Envoy<br/>Tenant Context Propagation]
    end
    
    MONOLITH -.-> AUTH_MS
    MONOLITH -.-> HOTEL_MS
    MONOLITH -.-> RESERVATION_MS
    MONOLITH -.-> PAYMENT_MS
    
    AUTH_MS --> MESH
    HOTEL_MS --> MESH
    RESERVATION_MS --> MESH
    PAYMENT_MS --> MESH
Loading

🔧 Implementation Examples

Tenant Context Propagation

// Utility for managing tenant context
export class TenantContext {
  private static context = new AsyncLocalStorage<{ organizationId: number }>()
  
  static run<T>(organizationId: number, callback: () => T): T {
    return this.context.run({ organizationId }, callback)
  }
  
  static getCurrentOrganizationId(): number | null {
    return this.context.getStore()?.organizationId || null
  }
  
  static requireOrganizationId(): number {
    const orgId = this.getCurrentOrganizationId()
    if (!orgId) {
      throw new Error('No tenant context available')
    }
    return orgId
  }
}

// Usage in middleware
export default class TenantContextMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    const organizationId = ctx.auth.user?.organization_id
    
    if (organizationId) {
      return TenantContext.run(organizationId, () => next())
    }
    
    return next()
  }
}

Tenant-Aware Repository Pattern

// Base repository with tenant awareness
export abstract class TenantAwareRepository<T extends BaseModel> {
  constructor(protected model: typeof BaseModel) {}
  
  protected getQuery() {
    const organizationId = TenantContext.getCurrentOrganizationId()
    const query = this.model.query()
    
    if (organizationId) {
      query.where('organization_id', organizationId)
    }
    
    return query
  }
  
  async findById(id: number): Promise<T | null> {
    return this.getQuery().where('id', id).first()
  }
  
  async create(data: Partial<T>): Promise<T> {
    const organizationId = TenantContext.requireOrganizationId()
    
    return this.model.create({
      ...data,
      organization_id: organizationId
    })
  }
}

// Specific repository implementation
export class HotelRepository extends TenantAwareRepository<Hotel> {
  constructor() {
    super(Hotel)
  }
  
  async findBySlug(slug: string): Promise<Hotel | null> {
    return this.getQuery().where('slug', slug).first()
  }
  
  async findActiveHotels(): Promise<Hotel[]> {
    return this.getQuery()
      .where('is_active', true)
      .where('is_deleted', false)
      .orderBy('name')
  }
}

Tenant Configuration Management

// Tenant-specific configuration
export class TenantConfig {
  private static cache = new Map<number, any>()
  
  static async get(organizationId: number, key: string, defaultValue?: any) {
    let config = this.cache.get(organizationId)
    
    if (!config) {
      const org = await Organization.find(organizationId)
      config = org?.settings || {}
      this.cache.set(organizationId, config)
    }
    
    return config[key] ?? defaultValue
  }
  
  static async getCheckInTime(organizationId: number): Promise<string> {
    return this.get(organizationId, 'default_check_in_time', '15:00')
  }
  
  static async getOverbookingPercentage(organizationId: number): Promise<number> {
    return this.get(organizationId, 'overbooking_percentage', 0)
  }
  
  static async getSupportedLanguages(organizationId: number): Promise<string[]> {
    return this.get(organizationId, 'supported_languages', ['en'])
  }
}

🧪 Testing Multi-Tenant Features

Test Data Setup

// Test utilities for multi-tenant scenarios
export class TenantTestUtils {
  static async createTestOrganization(overrides = {}) {
    return Organization.create({
      name: 'Test Organization',
      slug: `test-org-${Date.now()}`,
      email: '[email protected]',
      currency: 'USD',
      timezone: 'UTC',
      settings: {
        overbooking_percentage: 5,
        default_check_in_time: '14:00',
        default_check_out_time: '11:00'
      },
      ...overrides
    })
  }
  
  static async createTestUser(organizationId: number) {
    return User.create({
      full_name: 'Test User',
      email: `test-${Date.now()}@example.com`,
      password: 'password123',
      organization_id: organizationId
    })
  }
  
  static async withTenantContext<T>(
    organizationId: number, 
    callback: () => Promise<T>
  ): Promise<T> {
    await db.rawQuery('SELECT set_current_organization(?)', [organizationId])
    try {
      return await callback()
    } finally {
      await db.rawQuery('RESET app.current_organization')
    }
  }
}

Tenant Isolation Tests

// Test tenant data isolation
test('tenant data isolation', async ({ assert }) => {
  // Create two organizations
  const org1 = await TenantTestUtils.createTestOrganization({ name: 'Org 1' })
  const org2 = await TenantTestUtils.createTestOrganization({ name: 'Org 2' })
  
  // Create hotels for each organization
  const hotel1 = await TenantTestUtils.withTenantContext(org1.id, async () => {
    return Hotel.create({
      organization_id: org1.id,
      name: 'Hotel 1',
      slug: 'hotel-1'
    })
  })
  
  const hotel2 = await TenantTestUtils.withTenantContext(org2.id, async () => {
    return Hotel.create({
      organization_id: org2.id,
      name: 'Hotel 2',
      slug: 'hotel-2'
    })
  })
  
  // Test that org1 can only see its own hotels
  const org1Hotels = await TenantTestUtils.withTenantContext(org1.id, async () => {
    return Hotel.all()
  })
  
  assert.lengthOf(org1Hotels, 1)
  assert.equal(org1Hotels[0].id, hotel1.id)
  
  // Test that org2 can only see its own hotels
  const org2Hotels = await TenantTestUtils.withTenantContext(org2.id, async () => {
    return Hotel.all()
  })
  
  assert.lengthOf(org2Hotels, 1)
  assert.equal(org2Hotels[0].id, hotel2.id)
})

🔍 Monitoring & Observability

Tenant-Specific Metrics

// Metrics collection with tenant context
export class TenantMetrics {
  static async recordReservation(organizationId: number, hotelId: number) {
    await Metrics.increment('reservations.created', {
      organization_id: organizationId,
      hotel_id: hotelId
    })
  }
  
  static async recordRevenue(organizationId: number, amount: number, currency: string) {
    await Metrics.gauge('revenue.total', amount, {
      organization_id: organizationId,
      currency
    })
  }
  
  static async recordPerformance(organizationId: number, operation: string, duration: number) {
    await Metrics.histogram('operation.duration', duration, {
      organization_id: organizationId,
      operation
    })
  }
}

Tenant Health Monitoring

// Health checks per tenant
export class TenantHealthCheck {
  static async checkTenantHealth(organizationId: number) {
    const checks = await Promise.allSettled([
      this.checkDatabaseAccess(organizationId),
      this.checkCacheAccess(organizationId),
      this.checkBusinessRules(organizationId)
    ])
    
    return {
      organizationId,
      status: checks.every(c => c.status === 'fulfilled') ? 'healthy' : 'unhealthy',
      checks: checks.map((check, index) => ({
        name: ['database', 'cache', 'business_rules'][index],
        status: check.status,
        error: check.status === 'rejected' ? check.reason : null
      }))
    }
  }
  
  private static async checkDatabaseAccess(organizationId: number) {
    return TenantTestUtils.withTenantContext(organizationId, async () => {
      await Hotel.query().limit(1)
    })
  }
}

🚨 Troubleshooting

Common Multi-Tenant Issues

1. Missing Tenant Context

// Debug missing tenant context
export class TenantDebugger {
  static logMissingContext(operation: string) {
    const organizationId = TenantContext.getCurrentOrganizationId()
    
    if (!organizationId) {
      logger.warn(`Operation ${operation} executed without tenant context`, {
        stack: new Error().stack,
        timestamp: new Date().toISOString()
      })
    }
  }
}

2. Cross-Tenant Data Leaks

-- Query to detect potential data leaks
SELECT 
    table_name,
    COUNT(*) as total_rows,
    COUNT(DISTINCT organization_id) as tenant_count,
    COUNT(*) FILTER (WHERE organization_id IS NULL) as orphaned_rows
FROM (
    SELECT 'hotels' as table_name, organization_id FROM hotels
    UNION ALL
    SELECT 'reservations' as table_name, organization_id FROM reservations
    UNION ALL
    SELECT 'guests' as table_name, organization_id FROM guests
) t
GROUP BY table_name;

3. Performance Issues

// Monitor slow queries by tenant
export class TenantPerformanceMonitor {
  static async logSlowQuery(
    organizationId: number, 
    query: string, 
    duration: number
  ) {
    if (duration > 1000) { // Log queries > 1 second
      logger.warn('Slow query detected', {
        organization_id: organizationId,
        query,
        duration,
        timestamp: new Date().toISOString()
      })
    }
  }
}

📚 Best Practices

Development Guidelines

1. Always Include Tenant Context

// ✅ Good: Explicit tenant context
async function createReservation(data: ReservationData, organizationId: number) {
  return Reservation.create({
    ...data,
    organization_id: organizationId
  })
}

// ❌ Bad: Missing tenant context
async function createReservation(data: ReservationData) {
  return Reservation.create(data) // Could create orphaned records
}

2. Validate Tenant Access

// ✅ Good: Validate tenant ownership
async function updateHotel(hotelId: number, data: HotelData, user: User) {
  const hotel = await Hotel.query()
    .where('id', hotelId)
    .where('organization_id', user.organization_id)
    .firstOrFail()
  
  return hotel.merge(data).save()
}

// ❌ Bad: No tenant validation
async function updateHotel(hotelId: number, data: HotelData) {
  const hotel = await Hotel.findOrFail(hotelId)
  return hotel.merge(data).save()
}

3. Use Tenant-Aware Queries

// ✅ Good: Leverages RLS and explicit filtering
async function getHotelReservations(hotelId: number) {
  return Reservation.query()
    .where('hotel_id', hotelId)
    // organization_id filter applied by RLS
    .preload('guest')
    .orderBy('check_in_date')
}

Deployment Considerations

1. Database Migrations

// Ensure migrations work with existing tenant data
export default class extends BaseSchema {
  async up() {
    this.schema.alterTable('hotels', (table) => {
      // Add new column with default value
      table.string('new_field').defaultTo('default_value')
    })
    
    // Update existing records per tenant
    await this.defer(async (db) => {
      const organizations = await db.from('organizations').select('id')
      
      for (const org of organizations) {
        await db.from('hotels')
          .where('organization_id', org.id)
          .update('new_field', `tenant_${org.id}_value`)
      }
    })
  }
}

2. Backup Strategy

#!/bin/bash
# Tenant-aware backup script

# Full database backup (all tenants)
pg_dump innkeeper > backup_all_tenants_$(date +%Y%m%d).sql

# Per-tenant backup (if needed for compliance)
for org_id in $(psql -t -c "SELECT id FROM organizations WHERE is_active = true"); do
    pg_dump innkeeper \
        --where="organization_id = $org_id OR organization_id IS NULL" \
        > backup_tenant_${org_id}_$(date +%Y%m%d).sql
done

🔮 Future Enhancements

Planned Multi-Tenancy Improvements

1. Tenant-Specific Customizations

  • Custom fields per tenant
  • Tenant-specific UI themes
  • Configurable business workflows

2. Advanced Isolation Options

  • Geographic data residency
  • Tenant-specific encryption keys
  • Compliance-specific data handling

3. Performance Optimizations

  • Tenant-based database sharding
  • Intelligent query routing
  • Predictive resource scaling

📚 Related Documentation


🆘 Need Help?


← Previous: Database Schema | Wiki Home | Next: API Documentation →

⚠️ **GitHub.com Fallback** ⚠️