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:
- Identity & Tenancy - Spaces/Units owned by Org + Account
Depended On By:
- Availability & Calendars - Calendar management per Space/Unit
- Pricing & Revenue - Rate plans associated with Spaces/Units
- Bookings - Reservations reference Units
- Content & Metadata - Descriptions and media for Spaces/Units
- Channels & Distribution - Unit distribution to external marketplaces
- Search & Indexing - Searchable property indexes
Related Domains:
- Authorization - Space/unit access scoped by account membership
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
- User initiates Space creation via Admin UI or API
- Validate org_id and account_id exist and user has permission
- Create Space record with required fields (name, address, type)
- Auto-create default Unit named "{Space.name} - Main" with status='active'
- Create initial UnitSnapshot (version 1) for the default Unit
- Set Space status to 'draft' or 'active' based on completeness
- Emit event space.createdfor downstream consumers
- 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)
- User updates Unit via API (change name, capacity, description, etc.)
- Load current Unit state and increment version number
- Compute diff_hash by comparing new state to previous snapshot
- If diff_hash unchanged (no actual changes), return early (no-op)
- Create new UnitSnapshot with new version and complete state
- Update Unit record with new values and incremented version
- If Unit linked to ChannelListings, enqueue sync jobs per target
- Emit event unit.updatedwith unit_id, version, and diff_hash
- 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
- User selects amenities from catalog via UI
- 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
 
- Update Space.updated_at timestamp
- Emit event space.amenities_updatedwith space_id and amenity changes
- Trigger search reindex for Space (async)
- 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
- User initiates Space archive via Admin UI
- Validate no active bookings exist for Space's Units
- Check for future confirmed bookings - warn user if present
- Set Space.status = 'archived'
- Set all Units.status = 'archived'
- Do NOT delete any records - preserve full history
- Remove from search indexes (mark as inactive)
- Emit event space.archivedwith space_id
- 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
- Org/Account Ownership: Every Space and Unit MUST have valid org_id and account_id
- Space Isolation: All queries MUST filter by org_id (enforced via Row-Level Security or application layer)
- Default Unit: Every Space must have at least one Unit (created automatically)
- Version Monotonicity: Unit version numbers must increment by 1 on each update, never skip
- Snapshot Immutability: UnitSnapshot records never updated or deleted after creation
- Soft Deletes Only: Spaces and Units never hard deleted (set status to 'archived')
- Address Validation: Space address must include at minimum: city, region/state, country
- Capacity Validation: Unit max_guests must be > 0, bedrooms and bathrooms >= 0
- Amenity Uniqueness: A Space cannot have duplicate amenities (enforced by unique constraint)
- Status Transitions: Valid transitions: draft → active, active ↔ inactive, any → archived
- 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)
Related Documents
- MVP Mapping - Which MVP versions use this domain
- Identity & Tenancy - Org/Account ownership model
- Availability Domain - Calendar management
- Channels Domain - Hostaway integration and Unit snapshots
- Content Domain - Descriptions and media
- MVP.0 Overview
- V1 Vision