Skip to main content

Supply (Spaces & Units) - Domain Specification

First Introduced: MVP.0 Status: Specification Complete Last Updated: 2025-10-25


Overview

Supply (Spaces & Units) defines the inventory layer of the TVL platform — the physical properties and bookable assets that form the foundation of the hospitality business. This domain manages the hierarchical relationship between Spaces (physical properties/villas) and Units (bookable inventory within those spaces), along with their descriptive metadata, amenities, and versioned snapshots for synchronization.

Supply is the canonical source of truth for all property-related data, feeding into availability management, pricing, bookings, content presentation, and channel distribution. It maintains clear ownership boundaries through the org/account model and provides immutable version history to support idempotent channel synchronization and audit compliance.


Responsibilities

This domain IS responsible for:

  • Defining and managing physical property inventory (Spaces)
  • Managing bookable units within properties (Units)
  • Tracking property amenities and features
  • Maintaining version history of unit changes (Unit Snapshots)
  • Enforcing space/unit ownership at org and account level
  • Providing canonical property data to all downstream domains
  • Supporting versioned synchronization workflows for channel distribution

This domain is NOT responsible for:

  • Availability calendars and booking schedules (→ Availability domain)
  • Pricing rules and rate plans (→ Pricing domain)
  • Content presentation and media assets (→ Content domain)
  • Distribution to external channels (→ Channels domain)
  • Search indexing and discovery (→ Search domain)
  • Access control and permissions (→ Authorization domain)

Relationships

Depends On:

Depended On By:

Related Domains:


Core Concepts

Entity: Space

Purpose: Represents a physical property or estate (villa, retreat center, resort) that contains one or more bookable units.

Key Attributes:

  • id (UUID, primary key)
  • org_id (UUID, foreign key → organizations.id, required)
  • account_id (UUID, foreign key → accounts.id, required)
  • name (VARCHAR, required) - Property name
  • slug (VARCHAR, unique within org) - URL-friendly identifier
  • address (JSONB) - Structured address (street, city, region, country, postal_code, lat, lng)
  • status (ENUM) - active | inactive | archived | draft
  • property_type (ENUM) - villa | resort | retreat | apartment | other
  • settings (JSONB) - Space-level configuration
  • created_at, updated_at (timestamps)

Relationships:

  • Space → Org (*, many-to-one) - Every Space belongs to one Org
  • Space → Account (*, many-to-one) - Every Space owned by one Account
  • Space → Unit (1:*) - One Space contains one or more Units
  • Space → SpaceAmenity (1:*) - Many-to-many via junction table
  • Space → AvailabilityCalendar (1:1) - Each Space has one calendar (villa MVP)
  • Space → RatePlan (1:*) - Pricing rules per Space
  • Space → ChannelListing (1:*) - Distribution mappings

Lifecycle:

  • Created: During property onboarding flow
  • Updated: When property details change
  • Archived: Soft delete via status='archived' (never hard deleted for audit)
  • Reactivated: Can change from archived to active if needed

Business Rules:

  • Every Space must have valid org_id and account_id
  • Space slug must be unique within the Org
  • Space name required and must be non-empty
  • Archived Spaces retain all historical data and relationships
  • At least one active Unit required for Space to be bookable

Entity: Unit

Purpose: Represents bookable inventory within a Space — the actual asset that guests reserve. For villa MVP, typically one Unit per Space; future supports multi-unit properties (resorts, retreats).

Key Attributes:

  • id (UUID, primary key)
  • space_id (UUID, foreign key → spaces.id, required)
  • org_id (UUID, foreign key → organizations.id, required)
  • account_id (UUID, foreign key → accounts.id, required)
  • name (VARCHAR, required) - Unit name (e.g., "Main Villa", "Garden Suite")
  • unit_code (VARCHAR, unique within space) - Internal identifier
  • description (TEXT) - Short description
  • capacity (JSONB) - {max_guests, bedrooms, bathrooms, beds}
  • version (INTEGER, default 1) - Incremented on each update
  • status (ENUM) - active | inactive | archived | maintenance
  • settings (JSONB) - Unit-level configuration
  • created_at, updated_at (timestamps)

Relationships:

  • Unit → Space (*, many-to-one) - Every Unit belongs to one Space
  • Unit → Org (*, many-to-one) - Inherited from Space for query optimization
  • Unit → Account (*, many-to-one) - Inherited from Space for query optimization
  • Unit → UnitSnapshot (1:*) - Version history for sync and audit
  • Unit → Booking (1:*) - Reservations reference specific Units
  • Unit → ChannelListing (1:*) - Per-channel distribution (Hostaway, Airbnb, etc.)

Lifecycle:

  • Created: When Space is created (default Unit) or manually added
  • Updated: Triggers version increment and snapshot creation
  • Archived: Soft delete via status='archived'
  • Version tracking: Every update creates new snapshot record

Business Rules:

  • Unit must belong to valid Space
  • Unit inherits org_id and account_id from parent Space
  • Version number increments monotonically on each update
  • Cannot delete Unit with active bookings or holds
  • Unit updates trigger snapshot creation automatically
  • Capacity fields validated: max_guests > 0, bedrooms >= 0, bathrooms >= 0

Entity: UnitSnapshot

Purpose: Immutable version history of Unit state, enabling diff preview, audit trails, and replay workflows for channel synchronization.

Key Attributes:

  • id (UUID, primary key)
  • unit_id (UUID, foreign key → units.id, required)
  • version (INTEGER, required) - Matches unit.version at time of snapshot
  • snapshot (JSONB, required) - Complete Unit state including name, description, capacity, amenities, settings
  • diff_hash (VARCHAR(64)) - SHA-256 hash of changes from previous version
  • created_at (TIMESTAMP, required) - Snapshot timestamp
  • created_by (UUID, foreign key → users.id) - User who triggered the change

Relationships:

  • UnitSnapshot → Unit (*, many-to-one) - Many snapshots per Unit
  • UnitSnapshot → User (*, many-to-one) - Audit trail of who made changes
  • UnitSnapshot ← ChannelListing (1:*) - Referenced for idempotency tracking

Lifecycle:

  • Created: Automatically on every Unit update
  • Never updated or deleted - Immutable audit trail
  • Retention: 90 days default, configurable per org compliance requirements

Business Rules:

  • Snapshot created atomically with Unit update in same transaction
  • Snapshot JSONB contains complete Unit state at that version
  • diff_hash enables fast detection of actual changes vs. no-op updates
  • Snapshots never deleted even when Unit archived (audit compliance)
  • Version numbers must be sequential and monotonic

Entity: Amenity

Purpose: Catalog of property features and amenities (pool, wifi, air conditioning, parking, etc.) that describe Space capabilities.

Key Attributes:

  • id (UUID, primary key)
  • amenity_key (VARCHAR, unique) - Machine-readable identifier (e.g., "pool_private", "wifi_high_speed")
  • display_name (VARCHAR, required) - Human-readable label
  • category (ENUM) - essentials | comfort | entertainment | outdoor | safety | accessibility
  • icon (VARCHAR) - Icon identifier for UI rendering
  • description (TEXT) - Optional detailed description
  • is_active (BOOLEAN, default true) - Controls visibility in UI
  • sort_order (INTEGER) - Display ordering within category

Relationships:

  • Amenity → SpaceAmenity (1:*) - Many-to-many with Spaces via junction table
  • Amenity catalog is global (not org-scoped) for consistency

Lifecycle:

  • Created: Seeded during platform initialization
  • Updated: Catalog maintenance (rarely changed)
  • Deactivated: Set is_active = false (never deleted)

Business Rules:

  • Amenity keys must be globally unique
  • Amenity catalog shared across all Orgs for consistency
  • Display names can be localized (future enhancement)
  • MVP includes ~50 standard amenities across all categories

Entity: SpaceAmenity (Junction Table)

Purpose: Links Spaces to Amenities with optional parametric values (e.g., "parking: 3 spaces", "pool: heated").

Key Attributes:

  • id (UUID, primary key)
  • space_id (UUID, foreign key → spaces.id, required)
  • amenity_id (UUID, foreign key → amenities.id, required)
  • value (VARCHAR, nullable) - Optional parametric value
  • created_at (TIMESTAMP)

Relationships:

  • SpaceAmenity → Space (*, many-to-one)
  • SpaceAmenity → Amenity (*, many-to-one)

Lifecycle:

  • Created: When amenity added to Space
  • Deleted: When amenity removed from Space (hard delete acceptable)

Business Rules:

  • Unique constraint on (space_id, amenity_id) - no duplicate amenities per Space
  • Value field optional for boolean amenities (presence = true)
  • Value field used for quantifiable amenities (e.g., "3 bedrooms", "heated")

Workflows

Workflow: Create New Space

  1. User initiates Space creation via Admin UI or API
  2. Validate org_id and account_id exist and user has permission
  3. Create Space record with required fields (name, address, type)
  4. Auto-create default Unit named "{Space.name} - Main" with status='active'
  5. Create initial UnitSnapshot (version 1) for the default Unit
  6. Set Space status to 'draft' or 'active' based on completeness
  7. Emit event space.created for downstream consumers
  8. Return success with Space ID and default Unit ID

Postconditions:

  • Space has exactly 1 Unit (default)
  • Unit has exactly 1 Snapshot (version 1)
  • Space discoverable by users in same Org/Account
  • Space ready for amenity assignment, content addition, and calendar setup

Workflow: Update Unit (with Version Tracking)

  1. User updates Unit via API (change name, capacity, description, etc.)
  2. Load current Unit state and increment version number
  3. Compute diff_hash by comparing new state to previous snapshot
  4. If diff_hash unchanged (no actual changes), return early (no-op)
  5. Create new UnitSnapshot with new version and complete state
  6. Update Unit record with new values and incremented version
  7. If Unit linked to ChannelListings, enqueue sync jobs per target
  8. Emit event unit.updated with unit_id, version, and diff_hash
  9. Return success with new version number

Postconditions:

  • Unit version incremented
  • New immutable snapshot created
  • Channel sync jobs enqueued (if applicable)
  • Audit trail preserved
  • Previous versions remain accessible for diff/replay

Workflow: Add Amenities to Space

  1. User selects amenities from catalog via UI
  2. For each selected amenity:
    • Check if already assigned to Space (skip if exists)
    • Optionally provide parametric value (e.g., "heated", "3 spaces")
    • Create SpaceAmenity record linking Space to Amenity
  3. Update Space.updated_at timestamp
  4. Emit event space.amenities_updated with space_id and amenity changes
  5. Trigger search reindex for Space (async)
  6. Return success with updated amenity list

Postconditions:

  • Space amenities reflected in search filters
  • Content and channel feeds updated on next sync
  • Amenity changes auditable via events

Workflow: Archive Space

  1. User initiates Space archive via Admin UI
  2. Validate no active bookings exist for Space's Units
  3. Check for future confirmed bookings - warn user if present
  4. Set Space.status = 'archived'
  5. Set all Units.status = 'archived'
  6. Do NOT delete any records - preserve full history
  7. Remove from search indexes (mark as inactive)
  8. Emit event space.archived with space_id
  9. Return success with confirmation message

Postconditions:

  • Space not visible in active listings
  • Historical data preserved for audit and reporting
  • Can be reactivated by changing status back to 'active'
  • Existing bookings and financial records remain intact

Business Rules

  1. Org/Account Ownership: Every Space and Unit MUST have valid org_id and account_id
  2. Space Isolation: All queries MUST filter by org_id (enforced via Row-Level Security or application layer)
  3. Default Unit: Every Space must have at least one Unit (created automatically)
  4. Version Monotonicity: Unit version numbers must increment by 1 on each update, never skip
  5. Snapshot Immutability: UnitSnapshot records never updated or deleted after creation
  6. Soft Deletes Only: Spaces and Units never hard deleted (set status to 'archived')
  7. Address Validation: Space address must include at minimum: city, region/state, country
  8. Capacity Validation: Unit max_guests must be > 0, bedrooms and bathrooms >= 0
  9. Amenity Uniqueness: A Space cannot have duplicate amenities (enforced by unique constraint)
  10. Status Transitions: Valid transitions: draft → active, active ↔ inactive, any → archived
  11. Channel Sync Trigger: Unit updates with changed diff_hash trigger sync jobs for linked channels

Implementation Notes

MVP Scope (MVP.0)

Included:

  • Spaces, Units, UnitSnapshots, Amenities, SpaceAmenities entities
  • Basic CRUD operations for Spaces and Units
  • Automatic Unit Snapshot creation on every Unit update
  • Version tracking and diff_hash computation
  • Amenity catalog seeded with ~50 standard amenities
  • Org/Account ownership enforcement via foreign keys
  • Soft delete via status='archived'

Deferred:

  • Multi-unit management UI (supports via API but limited UI in MVP)
  • Bulk import/export tools (post-MVP)
  • Advanced property hierarchy (parent/child Spaces) - V1.0+
  • Geospatial search optimization (basic lat/lng support only)
  • Custom amenity creation (catalog-only in MVP)

Database Indexes

Critical for performance:

-- Spaces
CREATE INDEX idx_spaces_org_account ON spaces(org_id, account_id);
CREATE INDEX idx_spaces_status ON spaces(status) WHERE status = 'active';
CREATE UNIQUE INDEX idx_spaces_org_slug ON spaces(org_id, slug);
CREATE INDEX idx_spaces_location ON spaces USING GIST((address->>'lat')::float, (address->>'lng')::float);

-- Units
CREATE INDEX idx_units_space ON units(space_id);
CREATE INDEX idx_units_org_account ON units(org_id, account_id);
CREATE INDEX idx_units_status ON units(status) WHERE status = 'active';
CREATE INDEX idx_units_version ON units(unit_id, version DESC);

-- Unit Snapshots
CREATE INDEX idx_unit_snapshots_unit_version ON unit_snapshots(unit_id, version DESC);
CREATE INDEX idx_unit_snapshots_created_at ON unit_snapshots(created_at DESC);

-- Amenities
CREATE UNIQUE INDEX idx_amenities_key ON amenities(amenity_key);
CREATE INDEX idx_amenities_category ON amenities(category, sort_order);

-- Space Amenities
CREATE UNIQUE INDEX idx_space_amenities_unique ON space_amenities(space_id, amenity_id);
CREATE INDEX idx_space_amenities_amenity ON space_amenities(amenity_id);

Constraints

Enforce data integrity:

-- Spaces
ALTER TABLE spaces ADD CONSTRAINT spaces_name_not_empty CHECK (LENGTH(TRIM(name)) > 0);
ALTER TABLE spaces ADD CONSTRAINT spaces_slug_valid CHECK (slug ~ '^[a-z0-9-]+$');
ALTER TABLE spaces ADD CONSTRAINT spaces_address_required CHECK (
address ? 'city' AND address ? 'country'
);

-- Units
ALTER TABLE units ADD CONSTRAINT units_name_not_empty CHECK (LENGTH(TRIM(name)) > 0);
ALTER TABLE units ADD CONSTRAINT units_capacity_valid CHECK (
(capacity->>'max_guests')::int > 0 AND
(capacity->>'bedrooms')::int >= 0 AND
(capacity->>'bathrooms')::int >= 0
);
ALTER TABLE units ADD CONSTRAINT units_version_positive CHECK (version > 0);

-- Unit Snapshots
ALTER TABLE unit_snapshots ADD CONSTRAINT unit_snapshots_version_positive CHECK (version > 0);
ALTER TABLE unit_snapshots ADD CONSTRAINT unit_snapshots_snapshot_not_null CHECK (snapshot IS NOT NULL);

-- Space Amenities
ALTER TABLE space_amenities ADD CONSTRAINT space_amenities_unique UNIQUE (space_id, amenity_id);

Version Evolution

MVP.0 (Current - Villa Focus)

  • Single-unit Spaces (villa model)
  • Basic Space and Unit CRUD
  • Amenity catalog and assignment
  • Unit versioning and snapshots
  • Channel sync integration via snapshots

V1.0: Multi-Unit Properties

  • Enhanced UI for managing multiple Units per Space
  • Unit-level calendar management (separate from Space)
  • Bulk unit operations (create, update, archive)
  • Unit groups and categories (room types)

V1.1: Advanced Property Features

  • Custom amenity creation per Org
  • Property hierarchy (parent/child Spaces for resorts/brands)
  • Geospatial search optimization (PostGIS)
  • Media management integration (currently in Content domain)

V1.2: Enhanced Metadata

  • Structured floor plans and layouts
  • Accessibility compliance metadata
  • Sustainability certifications and ratings
  • Dynamic property attributes (seasonally closed areas)

V2.0: Multi-Location and Franchising

  • Property networks and brand relationships
  • Franchise management features
  • Cross-property amenity sharing
  • Regional property groupings

Physical Schema

See 001_initial_schema.sql for complete CREATE TABLE statements.

Summary:

  • 5 tables: spaces, units, unit_snapshots, amenities, space_amenities
  • 15+ indexes for query performance
  • 8+ constraints for data integrity
  • JSONB fields for flexible metadata (address, capacity, settings, snapshot)
  • Row-Level Security policies defined (not enforced in MVP)