Skip to main content

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:

Depended On By:

Related Domains:


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_id OR unit_id must 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_at must 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_id are immutable (only booking cancellation can delete)
  • Owner blocks require type='owner_block' and source='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_at must 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, and direction='export'
  • Import feeds require import_url and direction='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.

  1. Client requests availability for unit_id, check_in, check_out
  2. 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')
  3. 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, '[)')
  4. Return result: Available if both queries return 0 conflicts
  5. 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.

  1. Begin database transaction
  2. 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;
  3. Check for conflicts using availability query (see above)
  4. If conflicts exist:
    • Rollback transaction (lock auto-released)
    • Return error: "Unit not available for selected dates"
  5. 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)
  6. 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.

  1. Verify hold exists and status='active' and expires_at > now()
  2. Create booking record with status='confirmed'
  3. Update hold record: status='confirmed', confirmed_at=now()
  4. Update block record: type='booked', booking_id=<booking_id>
  5. Invalidate cached availability for affected date range
  6. Trigger iCal export regeneration for all feeds on calendar
  7. Publish event: booking.confirmed to 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.

  1. Fetch ical_feed configuration with direction='import' and import_status='active'
  2. HTTP GET external calendar with conditional headers:
    GET {import_url}
    If-None-Match: {import_etag}
    If-Modified-Since: {import_last_modified}
  3. If HTTP 304 Not Modified:
    • Update last_import_at=now()
    • Skip parsing (no changes)
  4. If HTTP 200 OK:
    • Parse iCalendar response (RFC 5545)
    • Extract VEVENT components with UID, DTSTART, DTEND, SUMMARY
    • Convert timezone to UTC if necessary
  5. For each VEVENT:
    • Lookup existing block by external_event_uid and external_calendar_url
    • If exists: Update start_at, end_at, reason, updated_at
    • If new: Insert block with type='external', source='ical_import'
  6. Delete orphaned blocks:
    DELETE FROM blocks
    WHERE external_calendar_url = {feed_url}
    AND external_event_uid NOT IN ({imported_uids})
  7. Update feed metadata:
    • import_etag = ETag header
    • import_last_modified = Last-Modified header
    • last_import_at = now()
    • import_status = 'active'
  8. 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.

  1. Receive request: GET /api/v1/ical/{export_token}.ics
  2. Validate export_token against ical_feeds.export_token_hash (bcrypt)
  3. If invalid: Return 401 Unauthorized
  4. Fetch feed configuration and associated calendar_id
  5. Query availability data based on audience:
    • public: Only confirmed bookings (summary="Unavailable")
    • owner: Bookings + holds + owner blocks (detailed summaries)
    • partner: Bookings + partner blocks
  6. 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
  7. 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
  8. 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
  9. Update feed stats: last_accessed_at=now(), increment access_count
  10. 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

  1. Half-Open Intervals: All time ranges use [start, end) notation — inclusive start, exclusive end
  2. UTC Storage: All timestamps stored in UTC; timezone conversion at API layer
  3. GIST Exclusion: Overlapping blocks/holds prevented by database constraint
  4. Precedence Hierarchy: Booking (confirmed) > Hold (active) > Block > Available
  5. Hold TTL: Default 15 minutes; maximum 60 minutes
  6. Lock TTL: Default 30 seconds; maximum 5 minutes
  7. Soft Deletes: Blocks soft-deleted via deleted_at for audit trail
  8. External Deduplication: iCal events identified by external_event_uid
  9. Sync Frequency: Default 60 minutes; minimum 15 minutes
  10. Error Threshold: Import status changes to 'error' after 3 consecutive failures
  11. Export Token Security: Tokens stored as bcrypt hashes; never logged or displayed
  12. 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:

  1. Expire Holds Job (every 60 seconds):

    UPDATE holds
    SET status = 'expired', updated_at = now()
    WHERE status = 'active' AND expires_at < now();
  2. 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
  3. 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