Availability & Calendar - Domain Specification
First Introduced: MVP.1 Status: Specification Complete Last Updated: 2025-10-25
Overview
The Availability & Calendar domain manages time-based inventory control for vacation rental properties. It defines when a Space or Unit can be booked and coordinates all forms of temporal blocking — confirmed bookings, temporary holds, owner blocks, maintenance windows, and external calendar synchronization via iCal feeds.
This domain is the authoritative source of truth for inventory availability, preventing double-bookings through database-level constraints and coordinated locking mechanisms. It bridges the gap between supply management (what exists) and booking operations (what's reserved), ensuring consistency across internal systems and external distribution channels.
Responsibilities
This domain IS responsible for:
- Managing availability calendars at Space and Unit levels
- Tracking confirmed bookings, holds, and blocks as time ranges
- Preventing overlapping reservations through exclusion constraints
- Synchronizing availability via iCal import/export (RFC 5545)
- Enforcing inventory locks during concurrent booking attempts
- Maintaining precedence rules (Booking > Hold > Block > Available)
- Tracking external calendar events and deduplication by UID
- Providing availability snapshots for search and discovery
This domain is NOT responsible for:
- Pricing calculations (→ Pricing domain)
- Payment processing (→ Payments domain)
- Guest lifecycle management (→ Bookings domain)
- Property content and descriptions (→ Content domain)
- Authorization and access control (→ Authorization domain)
- Channel-specific mapping logic (→ Channels domain)
Relationships
Depends On:
- Identity & Tenancy - All entities scoped by org_id
- Supply - Calendars belong to Spaces/Units
- Authorization - Permission checks for calendar operations
Depended On By:
- Bookings - Bookings create blocks and holds
- Pricing - Pricing queries availability windows
- Channels - iCal feeds exported to external platforms
- Search & Discovery - Availability flags in search indexes
Related Domains:
- Analytics & Audit - Calendar change audit trail
Core Concepts
Entity: AvailabilityCalendar
Purpose: Central object representing the complete availability state for a Space or Unit, aggregating all bookings, holds, and blocks.
Key Attributes:
- id(UUID, primary key)
- org_id(UUID, foreign key → organizations.id)
- space_id(UUID, nullable, foreign key → spaces.id)
- unit_id(UUID, nullable, foreign key → units.id)
- ical_import_url(TEXT, nullable) - External calendar source
- ical_import_etag(VARCHAR, nullable) - HTTP ETag for efficient sync
- ical_import_last_modified(TIMESTAMPTZ) - Last-Modified header value
- last_synced_at(TIMESTAMPTZ) - Last successful import timestamp
- sync_frequency_minutes(INTEGER, default 60) - How often to poll
- sync_status(ENUM) - active | paused | error | disabled
- sync_error_message(TEXT, nullable) - Last error details
- ical_export_token(VARCHAR, unique) - Secret token for export URL
- ical_export_token_hash(VARCHAR) - Bcrypt hash for validation
- ical_export_audience(ENUM) - public | owner | partner | internal
- calendar_type(ENUM) - standard | linked | external
- timezone(VARCHAR, default 'UTC') - Property timezone for display
- created_at,- updated_at(TIMESTAMPTZ)
Relationships:
- AvailabilityCalendar → Space (*, many-to-one, optional)
- AvailabilityCalendar → Unit (*, many-to-one, optional)
- AvailabilityCalendar → Block (1:*, one calendar has many blocks)
- AvailabilityCalendar → Hold (1:*, one calendar has many holds)
- AvailabilityCalendar → IcalFeed (1:*, one calendar has multiple feeds)
Lifecycle:
- Created: Automatically when Space/Unit is created
- Updated: When sync configuration changes or calendar accessed
- Deactivated: When Space/Unit is deactivated (cascade soft delete)
Business Rules:
- Exactly one of space_idORunit_idmust be set (XOR constraint)
- Each Space can have only one calendar (unique constraint)
- Each Unit can have only one calendar (unique constraint)
- Export tokens must be unique globally and stored hashed
- Sync status changes to 'error' after 3 consecutive failures
- Import URL must be valid HTTPS endpoint returning text/calendar
Constraints:
CONSTRAINT calendar_target_check CHECK (
  (space_id IS NOT NULL AND unit_id IS NULL) OR
  (space_id IS NULL AND unit_id IS NOT NULL)
)
CONSTRAINT uniq_calendar_space UNIQUE (org_id, space_id) WHERE space_id IS NOT NULL
CONSTRAINT uniq_calendar_unit UNIQUE (org_id, unit_id) WHERE unit_id IS NOT NULL
Entity: Block
Purpose: Represents a time range where a Space/Unit is unavailable, encompassing bookings, owner blocks, maintenance windows, and external calendar events.
Key Attributes:
- id(UUID, primary key)
- org_id(UUID, foreign key → organizations.id)
- space_id(UUID, nullable, foreign key → spaces.id)
- unit_id(UUID, nullable, foreign key → units.id)
- start_at(TIMESTAMPTZ, required) - Inclusive start
- end_at(TIMESTAMPTZ, required) - Exclusive end (half-open interval)
- type(ENUM) - booked | hold | owner_block | maintenance | external | system
- reason(TEXT, nullable) - Human-readable explanation
- source(ENUM) - manual | booking | ical_import | owner | partner | ops | system
- external_event_uid(VARCHAR, nullable) - iCal UID for deduplication
- external_calendar_url(TEXT, nullable) - Source feed URL
- booking_id(UUID, nullable, foreign key → bookings.id)
- hold_id(UUID, nullable, foreign key → holds.id)
- created_by(UUID, nullable, foreign key → users.id)
- created_at,- updated_at(TIMESTAMPTZ)
- deleted_at(TIMESTAMPTZ, nullable) - Soft delete for audit
Relationships:
- Block → Space (*, many-to-one, optional)
- Block → Unit (*, many-to-one, optional)
- Block → Booking (*, many-to-one, optional)
- Block → Hold (*, many-to-one, optional)
- Block → User (*, many-to-one, nullable)
Lifecycle:
- Created: Via booking confirmation, manual block, iCal import, or system rule
- Updated: When external sync modifies time range or reason
- Deleted: Soft delete when booking cancelled or block released
Business Rules:
- Time range must be half-open interval: [start_at, end_at)
- end_atmust be strictly greater than- start_at
- Cannot overlap with existing non-deleted blocks on same unit (GIST exclusion)
- External blocks (type='external') identified by external_event_uid
- Blocks with booking_idare immutable (only booking cancellation can delete)
- Owner blocks require type='owner_block'andsource='owner'
Constraints:
CONSTRAINT block_target_check CHECK (
  (space_id IS NOT NULL AND unit_id IS NULL) OR
  (space_id IS NULL AND unit_id IS NOT NULL)
)
CONSTRAINT block_time_check CHECK (end_at > start_at)
CONSTRAINT block_type_check CHECK (type IN ('booked', 'hold', 'owner_block', 'maintenance', 'external', 'system'))
-- GIST exclusion constraint prevents overlapping blocks on same unit
EXCLUDE USING GIST (
  unit_id WITH =,
  tstzrange(start_at, end_at, '[)') WITH &&
) WHERE (deleted_at IS NULL AND type NOT IN ('hold', 'external'))
Entity: Hold
Purpose: Temporary reservation lock acquired during the quote-to-booking flow, preventing double-booking while a guest completes payment.
Key Attributes:
- id(UUID, primary key)
- org_id(UUID, foreign key → organizations.id)
- quote_id(UUID, required, foreign key → quotes.id)
- space_id(UUID, nullable, foreign key → spaces.id)
- unit_id(UUID, nullable, foreign key → units.id)
- check_in(TIMESTAMPTZ, required)
- check_out(TIMESTAMPTZ, required)
- status(ENUM) - active | expired | confirmed | released | cancelled
- expires_at(TIMESTAMPTZ, required) - TTL for automatic release
- confirmed_at(TIMESTAMPTZ, nullable) - When converted to booking
- released_at(TIMESTAMPTZ, nullable) - When manually released
- lock_acquired_at(TIMESTAMPTZ, default now())
- lock_type(ENUM) - advisory | row_lock | application | redis
- created_by(UUID, nullable, foreign key → users.id)
- session_id(VARCHAR, nullable) - User session tracking
- created_at,- updated_at(TIMESTAMPTZ)
Relationships:
- Hold → Quote (*, many-to-one)
- Hold → Space (*, many-to-one, optional)
- Hold → Unit (*, many-to-one, optional)
- Hold → User (*, many-to-one, nullable)
Lifecycle:
- Created: When quote is generated and inventory locked
- Expired: Automatically via background job when expires_at < now()
- Confirmed: When booking created and payment succeeds
- Released: Manually by user abandoning quote or timeout
Business Rules:
- Default TTL is 15 minutes from creation
- expires_atmust be greater than- created_at
- Only status='active'holds block inventory
- Cannot overlap with other active holds on same unit (GIST exclusion)
- Expired holds release inventory automatically via scheduled job
- Maximum 1 active hold per quote (enforced at application layer)
Constraints:
CONSTRAINT hold_target_check CHECK (
  (space_id IS NOT NULL AND unit_id IS NULL) OR
  (space_id IS NULL AND unit_id IS NOT NULL)
)
CONSTRAINT hold_time_check CHECK (check_out > check_in)
CONSTRAINT hold_status_check CHECK (status IN ('active', 'expired', 'confirmed', 'released', 'cancelled'))
-- GIST exclusion constraint prevents overlapping active holds
EXCLUDE USING GIST (
  unit_id WITH =,
  tstzrange(check_in, check_out, '[)') WITH &&
) WHERE (status = 'active')
Entity: IcalFeed
Purpose: Configures bidirectional iCal synchronization — export feeds for external platforms and import feeds from other channels.
Key Attributes:
- id(UUID, primary key)
- org_id(UUID, foreign key → organizations.id)
- calendar_id(UUID, foreign key → availability_calendars.id)
- direction(ENUM) - export | import
- audience(VARCHAR, nullable) - public | owner | partner | internal (export only)
- export_token(VARCHAR, unique, nullable) - URL token for export
- export_token_hash(VARCHAR, nullable) - Bcrypt hash
- export_url(TEXT, nullable) - Generated URL path
- import_url(TEXT, nullable) - External calendar source
- import_etag(VARCHAR, nullable) - HTTP If-None-Match header
- import_last_modified(TIMESTAMPTZ, nullable) - If-Modified-Since header
- last_import_at(TIMESTAMPTZ, nullable)
- import_status(ENUM) - active | paused | error | disabled
- import_error(TEXT, nullable)
- include_bookings(BOOLEAN, default true)
- include_holds(BOOLEAN, default false)
- include_blocks(BOOLEAN, default true)
- include_types(VARCHAR[], nullable) - Array of block types to include
- last_accessed_at(TIMESTAMPTZ, nullable)
- access_count(INTEGER, default 0)
- sync_frequency_minutes(INTEGER, default 60)
- created_at,- updated_at(TIMESTAMPTZ)
Relationships:
- IcalFeed → AvailabilityCalendar (*, many-to-one)
Lifecycle:
- Created: When user enables iCal export or adds external feed
- Updated: When sync runs or configuration changes
- Paused: When sync errors exceed threshold (3 consecutive failures)
- Deleted: When calendar deleted (cascade) or user removes feed
Business Rules:
- Export feeds require export_token,audience, anddirection='export'
- Import feeds require import_urlanddirection='import'
- Export tokens must be unique globally and 64 characters
- Public feeds only include confirmed bookings (no holds/blocks)
- Owner feeds include all bookings, holds, and owner blocks
- Import status changes to 'error' after 3 consecutive failures
- ETag optimization skips import if remote calendar unchanged (304 response)
Constraints:
CONSTRAINT ical_direction_check CHECK (direction IN ('export', 'import'))
CONSTRAINT ical_export_fields CHECK (
  direction != 'export' OR (export_token IS NOT NULL AND audience IS NOT NULL)
)
CONSTRAINT ical_import_fields CHECK (
  direction != 'import' OR import_url IS NOT NULL
)
Entity: InventoryLock
Purpose: Explicit concurrency control mechanism preventing simultaneous overlapping write operations during booking creation or hold acquisition.
Key Attributes:
- id(UUID, primary key)
- org_id(UUID, foreign key → organizations.id)
- calendar_id(UUID, foreign key → availability_calendars.id)
- locked_start(TIMESTAMPTZ, required)
- locked_end(TIMESTAMPTZ, required)
- lock_type(ENUM) - booking | hold | block | manual
- resource_id(UUID, required) - ID of booking/hold/block being created
- acquired_at(TIMESTAMPTZ, default now())
- acquired_by(UUID, nullable, foreign key → users.id)
- session_id(VARCHAR, nullable)
- transaction_id(BIGINT, default pg_backend_pid())
- expires_at(TIMESTAMPTZ, required) - Auto-release after TTL
- released_at(TIMESTAMPTZ, nullable)
Relationships:
- InventoryLock → AvailabilityCalendar (*, many-to-one)
- InventoryLock → User (*, many-to-one, nullable)
Lifecycle:
- Created: At start of booking/hold transaction (before conflict check)
- Released: On transaction commit/rollback or explicit release
- Expired: Via background job if expires_at < now()and not released
Business Rules:
- Default TTL is 30 seconds
- Cannot acquire overlapping lock on same calendar (GIST exclusion)
- Locks automatically released when transaction ends (advisory lock)
- Expired locks cleaned up by scheduled job every 60 seconds
- Maximum lock duration is 5 minutes (enforced at application layer)
- Lock must be acquired before checking availability conflicts
Constraints:
CONSTRAINT lock_time_check CHECK (locked_end > locked_start)
CONSTRAINT lock_ttl_check CHECK (expires_at > acquired_at)
CONSTRAINT lock_type_check CHECK (lock_type IN ('booking', 'hold', 'block', 'manual'))
-- GIST exclusion constraint (THE CRITICAL DOUBLE-BOOKING PREVENTION)
EXCLUDE USING GIST (
  calendar_id WITH =,
  tstzrange(locked_start, locked_end, '[)') WITH &&
) WHERE (released_at IS NULL)
Workflows
Workflow: Check Availability
Purpose: Query whether a Space/Unit is available for a given date range.
- Client requests availability for unit_id, check_in, check_out
- Query blocks table for overlapping time ranges:
SELECT COUNT(*) = 0 AS is_available
 FROM blocks
 WHERE unit_id = $1
 AND tstzrange(start_at, end_at, '[)') && tstzrange($2, $3, '[)')
 AND deleted_at IS NULL
 AND type IN ('booked', 'owner_block', 'maintenance')
- Query holds table for active holds:
SELECT COUNT(*) = 0 AS no_holds
 FROM holds
 WHERE unit_id = $1
 AND status = 'active'
 AND expires_at > now()
 AND tstzrange(check_in, check_out, '[)') && tstzrange($2, $3, '[)')
- Return result: Available if both queries return 0 conflicts
- Cache result in Redis with 60-second TTL for performance
Postconditions:
- Client knows whether booking is possible
- No inventory state modified (read-only operation)
Workflow: Create Hold (Inventory Lock)
Purpose: Temporarily reserve inventory during quote generation, preventing double-booking.
- Begin database transaction
- Acquire inventory lock:
INSERT INTO inventory_locks (
 org_id, calendar_id, locked_start, locked_end,
 lock_type, resource_id, expires_at
 ) VALUES ($1, $2, $3, $4, 'hold', gen_random_uuid(), now() + interval '30 seconds')
 RETURNING id;
- Check for conflicts using availability query (see above)
- If conflicts exist:
- Rollback transaction (lock auto-released)
- Return error: "Unit not available for selected dates"
 
- If no conflicts:
- Create hold record with status='active',expires_at=now()+15min
- Create block record with type='hold',hold_id=<hold_id>
- Commit transaction (lock released)
 
- Create hold record with 
- Return hold_id to client with expiration timestamp
Postconditions:
- Hold created with 15-minute TTL
- Inventory blocked for specified time range
- Concurrent requests will fail due to GIST exclusion constraint
Workflow: Confirm Booking (Convert Hold)
Purpose: Convert an active hold into a confirmed booking after payment succeeds.
- Verify hold exists and status='active'andexpires_at > now()
- Create booking record with status='confirmed'
- Update hold record: status='confirmed',confirmed_at=now()
- Update block record: type='booked',booking_id=<booking_id>
- Invalidate cached availability for affected date range
- Trigger iCal export regeneration for all feeds on calendar
- Publish event: booking.confirmedto event bus
Postconditions:
- Hold converted to permanent booking
- Block type changed from 'hold' to 'booked'
- External calendars will receive updated iCal on next sync
- Inventory remains blocked indefinitely (until booking cancelled)
Workflow: Import External iCal Feed
Purpose: Synchronize external calendar events into internal blocks table, preventing double-booking across channels.
- Fetch ical_feed configuration with direction='import'andimport_status='active'
- HTTP GET external calendar with conditional headers:
GET {import_url}
 If-None-Match: {import_etag}
 If-Modified-Since: {import_last_modified}
- If HTTP 304 Not Modified:
- Update last_import_at=now()
- Skip parsing (no changes)
 
- Update 
- If HTTP 200 OK:
- Parse iCalendar response (RFC 5545)
- Extract VEVENT components with UID, DTSTART, DTEND, SUMMARY
- Convert timezone to UTC if necessary
 
- For each VEVENT:
- Lookup existing block by external_event_uidandexternal_calendar_url
- If exists: Update start_at,end_at,reason,updated_at
- If new: Insert block with type='external',source='ical_import'
 
- Lookup existing block by 
- Delete orphaned blocks:
DELETE FROM blocks
 WHERE external_calendar_url = {feed_url}
 AND external_event_uid NOT IN ({imported_uids})
- Update feed metadata:
- import_etag= ETag header
- import_last_modified= Last-Modified header
- last_import_at= now()
- import_status= 'active'
 
- On error: Increment failure counter, set import_status='error'after 3 failures
Postconditions:
- External events synchronized as blocks
- Deduplication by UID prevents duplicates
- Deleted external events removed from internal calendar
- ETag optimization reduces bandwidth on subsequent syncs
Workflow: Export iCal Feed
Purpose: Generate RFC 5545-compliant iCalendar feed for external consumption.
- Receive request: GET /api/v1/ical/{export_token}.ics
- Validate export_token against ical_feeds.export_token_hash(bcrypt)
- If invalid: Return 401 Unauthorized
- Fetch feed configuration and associated calendar_id
- Query availability data based on audience:- public: Only confirmed bookings (summary="Unavailable")
- owner: Bookings + holds + owner blocks (detailed summaries)
- partner: Bookings + partner blocks
 
- Generate VCALENDAR structure:
BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//The Villa Life//TVL Platform//EN
 CALSCALE:GREGORIAN
 METHOD:PUBLISH
 X-WR-CALNAME:TVL Calendar - {audience}
 X-WR-TIMEZONE:UTC
- For each availability record:
BEGIN:VEVENT
 UID:{type}-{id}@thevillalife.com
 DTSTAMP:{updated_at}
 DTSTART:{start_at}
 DTEND:{end_at}
 SUMMARY:{summary}
 STATUS:CONFIRMED
 TRANSP:OPAQUE
 END:VEVENT
- Set response headers:
- Content-Type: text/calendar; charset=utf-8
- ETag: {hash_of_content}
- Last-Modified: {most_recent_update}
- Cache-Control: private, max-age=3600
 
- Update feed stats: last_accessed_at=now(), incrementaccess_count
- Return iCalendar content
Postconditions:
- External platform receives current availability state
- ETag supports efficient conditional requests (304 Not Modified)
- Access tracked for monitoring and analytics
Business Rules
- Half-Open Intervals: All time ranges use [start, end)notation — inclusive start, exclusive end
- UTC Storage: All timestamps stored in UTC; timezone conversion at API layer
- GIST Exclusion: Overlapping blocks/holds prevented by database constraint
- Precedence Hierarchy: Booking (confirmed) > Hold (active) > Block > Available
- Hold TTL: Default 15 minutes; maximum 60 minutes
- Lock TTL: Default 30 seconds; maximum 5 minutes
- Soft Deletes: Blocks soft-deleted via deleted_atfor audit trail
- External Deduplication: iCal events identified by external_event_uid
- Sync Frequency: Default 60 minutes; minimum 15 minutes
- Error Threshold: Import status changes to 'error' after 3 consecutive failures
- Export Token Security: Tokens stored as bcrypt hashes; never logged or displayed
- Calendar Granularity: One calendar per Space (MVP); Unit-level in V1.0
Implementation Notes
MVP Scope (MVP.1)
Included:
- AvailabilityCalendar entity with Space-level calendars
- Block entity with types: booked, hold, owner_block, maintenance, external
- Hold entity with TTL expiration and status lifecycle
- Basic iCal export (public audience, confirmed bookings only)
- Basic iCal import with ETag optimization
- GIST exclusion constraints on blocks and holds tables
- PostgreSQL advisory locks for inventory locking (Option 1)
- Availability check query function
- Hold expiration background job
Deferred to V1.0:
- Unit-level calendars (multi-room properties)
- InventoryLock explicit table (using advisory locks in MVP.1)
- Multiple export feeds per calendar (only one public feed)
- VFREEBUSY support
- Materialized availability views (direct queries only)
- Advanced conflict resolution UI
- Bulk calendar operations
- Changeover rules and cleaning buffers
Database Indexes
Critical for MVP.1:
-- GIST indexes for range overlap detection
CREATE INDEX idx_blocks_time_range_gist ON blocks USING GIST (
  unit_id, tstzrange(start_at, end_at, '[)')
) WHERE deleted_at IS NULL;
CREATE INDEX idx_holds_time_range_gist ON holds USING GIST (
  unit_id, tstzrange(check_in, check_out, '[)')
) WHERE status = 'active';
-- Composite index for availability checks
CREATE INDEX idx_blocks_availability_check ON blocks(unit_id, start_at, end_at)
WHERE deleted_at IS NULL;
-- Index for hold expiration job
CREATE INDEX idx_holds_expiration ON holds(expires_at)
WHERE status = 'active';
-- Index for external event deduplication
CREATE INDEX idx_blocks_external_uid ON blocks(external_event_uid, external_calendar_url)
WHERE external_event_uid IS NOT NULL;
-- Index for iCal sync job
CREATE INDEX idx_calendars_sync_pending ON availability_calendars(last_synced_at)
WHERE sync_status = 'active' AND ical_import_url IS NOT NULL;
Constraints
Enforce data integrity:
-- Enable btree_gist extension
CREATE EXTENSION IF NOT EXISTS btree_gist;
-- Blocks: Prevent overlapping non-hold, non-external blocks on same unit
ALTER TABLE blocks ADD CONSTRAINT no_overlapping_blocks_per_unit EXCLUDE USING GIST (
  unit_id WITH =,
  tstzrange(start_at, end_at, '[)') WITH &&
) WHERE (deleted_at IS NULL AND type NOT IN ('hold', 'external'));
-- Holds: Prevent overlapping active holds on same unit
ALTER TABLE holds ADD CONSTRAINT no_overlapping_active_holds EXCLUDE USING GIST (
  unit_id WITH =,
  tstzrange(check_in, check_out, '[)') WITH &&
) WHERE (status = 'active');
-- Half-open interval enforcement
ALTER TABLE blocks ADD CONSTRAINT block_interval_format CHECK (
  lower_inc(tstzrange(start_at, end_at)) = true AND
  upper_inc(tstzrange(start_at, end_at)) = false
);
Background Jobs
Scheduled tasks:
- 
Expire Holds Job (every 60 seconds): UPDATE holds
 SET status = 'expired', updated_at = now()
 WHERE status = 'active' AND expires_at < now();
- 
Import iCal Feeds Job (every 15 minutes): SELECT id FROM ical_feeds
 WHERE direction = 'import'
 AND import_status = 'active'
 AND (last_import_at IS NULL OR last_import_at < now() - (sync_frequency_minutes || ' minutes')::interval);
 -- For each feed, execute import workflow
- 
Cleanup Expired Locks Job (every 60 seconds, V1.0 only): UPDATE inventory_locks
 SET released_at = now()
 WHERE released_at IS NULL AND expires_at < now();
Future Enhancements
V1.0: Unit-Level Calendars & Advanced Blocking
- Support multi-room/multi-unit properties with unit-level calendars
- Aggregate availability at Space level (all units, partial units)
- Changeover rules engine (same-day turnover, cleaning buffers)
- Explicit InventoryLock table for observability (replace advisory locks)
- Multiple export feeds per calendar (public, owner, partner)
- Materialized daily availability views for search performance
- Conflict resolution API endpoint with override capabilities
V1.1: Advanced iCal & Performance
- VFREEBUSY endpoint for availability queries
- Recurring event support (RRULE parsing)
- VTIMEZONE component handling
- Optimistic concurrency control (version columns)
- Database partitioning by year for blocks table
- Redis caching layer for hot availability queries
- CDN distribution for public iCal feeds
V1.2: Operational Tools
- Bulk calendar operations (block multiple units, date ranges)
- Calendar comparison/conflict detection UI
- Analytics: occupancy rate, booking lead time
- Min/max stay enforcement at database level
- Seasonal availability rules
- Dynamic pricing integration with availability
V2.0: Multi-Region & Advanced Features
- Multi-region database replication
- Distributed locking via Redis (replace advisory locks)
- AI-powered conflict resolution
- Smart availability recommendations
- Cross-property availability aggregation
- Advanced analytics and forecasting
Physical Schema
See 001_initial_schema.sql for complete CREATE TABLE statements.
Summary:
- 5 tables: availability_calendars, blocks, holds, ical_feeds, inventory_locks (V1.0)
- 15+ indexes (including GIST for range queries)
- 10+ constraints (including GIST exclusion for overlap prevention)
- 3 background jobs (hold expiration, iCal sync, lock cleanup)
- Row-Level Security policies for multi-tenancy
Related Documents
- MVP Mapping - Which MVP versions use this domain
- Supply Domain - Properties and Units
- Bookings Domain - Booking lifecycle
- Channels Domain - External distribution
- Deep Dive Report - Comprehensive analysis
- GIST Constraints Guide - Implementation patterns
- iCal RFC 5545 - iCalendar specification
- MVP.1 Overview
- V1 Vision