Skip to the content.

Industry Standards Quick Reference

Quick lookup for developers implementing PMS features


🏗️ Multi-Tenant Architecture

Pattern: Shared Database, Row-Level Isolation

✅ CORRECT                          ❌ WRONG
────────────────────────────────    ──────────────────────
WHERE tenant_id = :tenant_id        WHERE id = :property_id
  AND property_id = :id             (Missing tenant_id!)

Tenant → Properties → Rooms Hierarchy

Marriott International (Tenant)
  └─ Marriott Boston (Property)
      └─ Room 101, 102, ... (Rooms)

Every table must have: tenant_id UUID NOT NULL REFERENCES tenants(id)


📊 Standard Tables (22 Total)

Core Multi-Tenancy (3)

Property Management (6)

Reservations (3)

Financial (4)

Operations (3)


🔑 Standard ENUM Types (20)

Status Types

tenant_status: TRIAL | ACTIVE | SUSPENDED | INACTIVE | CANCELLED
reservation_status: PENDING | CONFIRMED | CHECKED_IN | CHECKED_OUT | CANCELLED | NO_SHOW
payment_status: PENDING | PROCESSING | COMPLETED | FAILED | CANCELLED | REFUNDED
room_status: AVAILABLE | OCCUPIED | DIRTY | CLEAN | INSPECTED | OUT_OF_ORDER

Strategy Types

rate_strategy: FIXED | DYNAMIC | SEASONAL | WEEKEND | LASTMINUTE | EARLYBIRD
reservation_source: DIRECT | WEBSITE | PHONE | WALKIN | OTA | CORPORATE | GROUP

Classification

tenant_type: INDEPENDENT | CHAIN | FRANCHISE | MANAGEMENT_COMPANY
tenant_role: OWNER | ADMIN | MANAGER | STAFF | VIEWER

💡 Standard Query Patterns

Get Properties for Tenant

SELECT * FROM properties
WHERE tenant_id = :tenant_id
  AND deleted_at IS NULL
ORDER BY name;

Search Available Rooms

SELECT rt.*, ra.available_rooms
FROM room_types rt
JOIN availability.room_availability ra ON ra.room_type_id = rt.id
WHERE rt.tenant_id = :tenant_id
  AND rt.property_id = :property_id
  AND ra.date BETWEEN :check_in AND :check_out
  AND ra.available_rooms > 0
  AND rt.is_active = true
  AND rt.deleted_at IS NULL;

Get Guest Reservations

SELECT r.*, p.name as property_name, rt.name as room_type_name
FROM reservations r
JOIN properties p ON r.property_id = p.id
JOIN room_types rt ON r.room_type_id = rt.id
WHERE r.tenant_id = :tenant_id
  AND r.guest_id = :guest_id
  AND r.status IN ('CONFIRMED', 'CHECKED_IN')
ORDER BY r.check_in_date DESC;

Calculate Occupancy Rate

SELECT
    date,
    (booked_rooms::float / total_rooms::float * 100) as occupancy_rate
FROM availability.room_availability
WHERE tenant_id = :tenant_id
  AND property_id = :property_id
  AND date BETWEEN :start_date AND :end_date
ORDER BY date;

📈 Standard KPIs

Occupancy Rate

Formula: (Rooms Sold / Rooms Available) × 100
Industry Average: 60-70%

ADR (Average Daily Rate)

Formula: Total Room Revenue / Rooms Sold
Industry Average: $100-300 (varies by market)

RevPAR (Revenue Per Available Room)

Formula: Total Room Revenue / Rooms Available
Alternative: ADR × Occupancy Rate
Industry Average: $60-200

Cancellation Rate

Formula: (Cancelled Reservations / Total Reservations) × 100
Industry Average: 10-40%

🔐 Security Checklist

✅ Required for Every Query

❌ Never Do This

-- ❌ Missing tenant_id
SELECT * FROM reservations WHERE id = :id;

-- ❌ String interpolation (SQL injection!)
query = f"SELECT * FROM users WHERE username = '{username}'";

-- ❌ Returning all tenants' data
SELECT * FROM properties WHERE is_active = true;

🌐 Industry Provider Comparison

Feature Oracle OPERA Cloudbeds Protel RMS Cloud Tartware
Multi-tenant
Multi-property
Cloud-native
Real-time availability
Channel manager
Dynamic pricing
Analytics/KPIs
GDPR compliant

📱 API Patterns (RESTful)

Standard Endpoints

# Tenants
GET    /api/v1/tenants/:tenantId

# Properties
GET    /api/v1/tenants/:tenantId/properties
GET    /api/v1/tenants/:tenantId/properties/:propertyId
POST   /api/v1/tenants/:tenantId/properties
PUT    /api/v1/tenants/:tenantId/properties/:propertyId
DELETE /api/v1/tenants/:tenantId/properties/:propertyId

# Reservations
GET    /api/v1/tenants/:tenantId/reservations
POST   /api/v1/tenants/:tenantId/reservations
GET    /api/v1/tenants/:tenantId/reservations/:reservationId
PUT    /api/v1/tenants/:tenantId/reservations/:reservationId
DELETE /api/v1/tenants/:tenantId/reservations/:reservationId

# Availability
GET    /api/v1/tenants/:tenantId/availability?property_id=X&start_date=Y&end_date=Z
POST   /api/v1/tenants/:tenantId/availability/search

🎯 Common Use Cases

1. Create New Reservation

BEGIN;
  -- Insert reservation
  INSERT INTO reservations (
    tenant_id, property_id, guest_id, room_type_id,
    check_in_date, check_out_date, status
  ) VALUES (...) RETURNING id;

  -- Update availability
  UPDATE availability.room_availability
  SET available_rooms = available_rooms - 1,
      booked_rooms = booked_rooms + 1
  WHERE tenant_id = :tenant_id
    AND property_id = :property_id
    AND room_type_id = :room_type_id
    AND date BETWEEN :check_in AND :check_out;
COMMIT;

2. Check-in Guest

UPDATE reservations
SET status = 'CHECKED_IN',
    checked_in_at = CURRENT_TIMESTAMP,
    room_id = :assigned_room_id,
    updated_by = :user_id
WHERE id = :reservation_id
  AND tenant_id = :tenant_id
  AND status = 'CONFIRMED';

-- Log status change
INSERT INTO reservation_status_history (
  reservation_id, tenant_id,
  old_status, new_status, changed_by
) VALUES (
  :reservation_id, :tenant_id,
  'CONFIRMED', 'CHECKED_IN', :user_id
);

3. Process Payment

INSERT INTO payments (
  tenant_id, reservation_id,
  amount, currency, method, status, transaction_type
) VALUES (
  :tenant_id, :reservation_id,
  :amount, 'USD', 'CREDIT_CARD', 'COMPLETED', 'CHARGE'
);

-- Update reservation paid amount
UPDATE reservations
SET paid_amount = paid_amount + :amount,
    balance_due = total_amount - (paid_amount + :amount)
WHERE id = :reservation_id
  AND tenant_id = :tenant_id;

📊 JSONB Usage Patterns

Property Metadata

-- Store flexible configuration
UPDATE properties
SET metadata = '{
  "brand": "Marriott Hotels",
  "amenities": ["WiFi", "Pool", "Gym"],
  "checkInTime": "15:00",
  "checkOutTime": "11:00",
  "policies": {
    "cancellation": "24h",
    "pets": "allowed"
  }
}'::jsonb
WHERE id = :property_id;

-- Query JSONB
SELECT * FROM properties
WHERE metadata->>'brand' = 'Marriott Hotels'
  AND metadata->'amenities' ? 'Pool';

🔧 Performance Optimization

Critical Indexes

-- Tenant isolation (ALL tables)
CREATE INDEX idx_table_tenant ON table_name(tenant_id);

-- Lookup indexes
CREATE INDEX idx_reservations_confirmation ON reservations(confirmation_number);
CREATE INDEX idx_reservations_dates ON reservations(check_in_date, check_out_date);

-- Composite indexes
CREATE INDEX idx_availability_lookup
ON availability.room_availability(tenant_id, property_id, date);

Query Optimization

-- ✅ Good: Uses index
EXPLAIN ANALYZE
SELECT * FROM reservations
WHERE tenant_id = :tenant_id
  AND status = 'CONFIRMED';

-- Check for: Index Scan (good) vs Seq Scan (bad)


Quick Reference Version: 1.0.0 Print/PDF: Optimized for quick lookup Last Updated: October 15, 2025