{
 "$schema": "https://rapideyeinspections.com/blog/str-operations-data-model/schema.json",
 "name": "STR Operations Data Model",
 "version": "0.1",
 "published": "2026-07-31",
 "publisher": "RapidEye Research",
 "canonical": "https://rapideyeinspections.com/blog/str-operations-data-model/",
 "formats": {
  "sql": "https://rapideyeinspections.com/blog/str-operations-data-model/schema.sql",
  "markdown": "https://rapideyeinspections.com/blog/str-operations-data-model/schema.md"
 },
 "license": "Free to use, adapt, and build on, with or without attribution.",
 "design_principles": [
  "The PMS stays the system of record for reservations; the booking layer is a one-way synced mirror keyed by (source, external_id) for idempotent ingestion.",
  "The turnover is the pivot entity between the departing and arriving reservation; findings attribute to the stay that just ended.",
  "Condition media is first-class: device capture time stored separately from upload time, sha256 content hash for dedup and reuse detection.",
  "Checklists are versioned and immutable; completed work pins the version it was performed against.",
  "All timestamps UTC; the property carries its IANA timezone; convert at render time."
 ],
 "entities": {
  "properties": {
   "layer": "property",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "address_line1",
     "type": "text"
    },
    {
     "name": "address_line2",
     "type": "text"
    },
    {
     "name": "city",
     "type": "text"
    },
    {
     "name": "region",
     "type": "text"
    },
    {
     "name": "postal_code",
     "type": "text"
    },
    {
     "name": "country_code",
     "type": "text",
     "note": "ISO 3166-1 alpha-2"
    },
    {
     "name": "timezone",
     "type": "text",
     "required": true,
     "note": "IANA, e.g. America/New_York"
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "active",
      "onboarding",
      "offboarded"
     ]
    },
    {
     "name": "source",
     "type": "text",
     "note": "PMS this property syncs from"
    },
    {
     "name": "external_id",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ],
   "unique": [
    [
     "source",
     "external_id"
    ]
   ]
  },
  "units": {
   "layer": "property",
   "note": "All operations tables reference units, never properties directly; a single house still gets one unit row.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "references": "properties",
     "required": true
    },
    {
     "name": "name",
     "type": "text",
     "default": "Main"
    },
    {
     "name": "bedrooms",
     "type": "numeric"
    },
    {
     "name": "bathrooms",
     "type": "numeric"
    },
    {
     "name": "max_occupancy",
     "type": "int"
    },
    {
     "name": "floor_area_m2",
     "type": "numeric"
    },
    {
     "name": "status",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "listings": {
   "layer": "property",
   "note": "A unit's presence on a channel; ical_url is the universal fallback every major channel exports.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "channel",
     "type": "text",
     "enum": [
      "airbnb",
      "vrbo",
      "booking",
      "direct",
      "other"
     ],
     "required": true
    },
    {
     "name": "external_id",
     "type": "text"
    },
    {
     "name": "url",
     "type": "text"
    },
    {
     "name": "ical_url",
     "type": "text"
    },
    {
     "name": "status",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ],
   "unique": [
    [
     "channel",
     "external_id"
    ]
   ]
  },
  "guests": {
   "layer": "booking",
   "note": "Minimal PII on purpose; an operations tool attributes stays, it does not run a CRM.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "full_name",
     "type": "text"
    },
    {
     "name": "email",
     "type": "text"
    },
    {
     "name": "phone",
     "type": "text"
    },
    {
     "name": "source",
     "type": "text"
    },
    {
     "name": "external_id",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ],
   "unique": [
    [
     "source",
     "external_id"
    ]
   ]
  },
  "reservations": {
   "layer": "booking",
   "note": "One-way synced mirror; never write bookings here first.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "listing_id",
     "type": "uuid",
     "references": "listings"
    },
    {
     "name": "guest_id",
     "type": "uuid",
     "references": "guests"
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "confirmed",
      "cancelled",
      "completed",
      "no_show"
     ],
     "required": true
    },
    {
     "name": "check_in",
     "type": "timestamptz",
     "required": true
    },
    {
     "name": "check_out",
     "type": "timestamptz",
     "required": true,
     "note": "must be after check_in"
    },
    {
     "name": "adults",
     "type": "int"
    },
    {
     "name": "children",
     "type": "int"
    },
    {
     "name": "pets",
     "type": "int"
    },
    {
     "name": "channel",
     "type": "text"
    },
    {
     "name": "source",
     "type": "text",
     "required": true
    },
    {
     "name": "external_id",
     "type": "text",
     "required": true
    },
    {
     "name": "synced_at",
     "type": "timestamptz"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ],
   "unique": [
    [
     "source",
     "external_id"
    ]
   ],
   "indexes": [
    [
     "unit_id",
     "check_in",
     "check_out"
    ]
   ]
  },
  "vendors": {
   "layer": "people",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "kind",
     "type": "text",
     "enum": [
      "cleaning",
      "maintenance",
      "inspection",
      "other"
     ],
     "required": true
    },
    {
     "name": "email",
     "type": "text"
    },
    {
     "name": "phone",
     "type": "text"
    },
    {
     "name": "status",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "staff": {
   "layer": "people",
   "note": "In-house staff have vendor_id null; vendor personnel reference their company.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "vendor_id",
     "type": "uuid",
     "references": "vendors"
    },
    {
     "name": "full_name",
     "type": "text",
     "required": true
    },
    {
     "name": "role",
     "type": "text",
     "enum": [
      "cleaner",
      "inspector",
      "maintenance",
      "manager",
      "admin"
     ],
     "required": true
    },
    {
     "name": "email",
     "type": "text"
    },
    {
     "name": "phone",
     "type": "text"
    },
    {
     "name": "status",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "checklists": {
   "layer": "sop",
   "note": "One row is one immutable VERSION of one SOP; editing inserts version+1.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "kind",
     "type": "text",
     "enum": [
      "cleaning",
      "inspection",
      "maintenance",
      "custom"
     ],
     "required": true
    },
    {
     "name": "version",
     "type": "int",
     "required": true
    },
    {
     "name": "is_current",
     "type": "boolean"
    }
   ],
   "unique": [
    [
     "name",
     "version"
    ]
   ]
  },
  "checklist_items": {
   "layer": "sop",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "checklist_id",
     "type": "uuid",
     "references": "checklists",
     "required": true
    },
    {
     "name": "section",
     "type": "text"
    },
    {
     "name": "position",
     "type": "int",
     "required": true
    },
    {
     "name": "instruction",
     "type": "text",
     "required": true
    },
    {
     "name": "requires_photo",
     "type": "boolean"
    },
    {
     "name": "requires_video",
     "type": "boolean"
    }
   ]
  },
  "turnovers": {
   "layer": "operations",
   "note": "The pivot of STR operations: the window between the departing stay and the arriving one. Findings attribute to reservation_out.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "reservation_out_id",
     "type": "uuid",
     "references": "reservations",
     "note": "the stay that just ended"
    },
    {
     "name": "reservation_in_id",
     "type": "uuid",
     "references": "reservations",
     "note": "the stay about to begin"
    },
    {
     "name": "window_start",
     "type": "timestamptz"
    },
    {
     "name": "window_end",
     "type": "timestamptz"
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "pending",
      "scheduled",
      "in_progress",
      "done",
      "failed_inspection"
     ]
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "tasks": {
   "layer": "operations",
   "note": "Tasks can exist outside a turnover (deep cleans, preventive maintenance): turnover_id nullable, unit_id not.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "turnover_id",
     "type": "uuid",
     "references": "turnovers"
    },
    {
     "name": "kind",
     "type": "text",
     "enum": [
      "cleaning",
      "inspection",
      "maintenance",
      "delivery",
      "custom"
     ],
     "required": true
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "open",
      "assigned",
      "in_progress",
      "done",
      "verified",
      "cancelled"
     ]
    },
    {
     "name": "assignee_id",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "checklist_id",
     "type": "uuid",
     "references": "checklists",
     "note": "pins the exact SOP version performed"
    },
    {
     "name": "due_at",
     "type": "timestamptz"
    },
    {
     "name": "started_at",
     "type": "timestamptz"
    },
    {
     "name": "completed_at",
     "type": "timestamptz"
    },
    {
     "name": "verified_by_id",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "notes",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "task_item_results": {
   "layer": "operations",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "task_id",
     "type": "uuid",
     "references": "tasks",
     "required": true
    },
    {
     "name": "checklist_item_id",
     "type": "uuid",
     "references": "checklist_items",
     "required": true
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "done",
      "skipped",
      "flagged"
     ],
     "required": true
    },
    {
     "name": "note",
     "type": "text"
    },
    {
     "name": "completed_at",
     "type": "timestamptz"
    }
   ],
   "unique": [
    [
     "task_id",
     "checklist_item_id"
    ]
   ]
  },
  "findings": {
   "layer": "operations",
   "note": "attributed_reservation_id is copied from the parent turnover's reservation_out_id at creation; denormalized so attribution survives later edits.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "task_id",
     "type": "uuid",
     "references": "tasks"
    },
    {
     "name": "attributed_reservation_id",
     "type": "uuid",
     "references": "reservations"
    },
    {
     "name": "category",
     "type": "text",
     "enum": [
      "damage",
      "cleanliness",
      "maintenance",
      "missing_item",
      "safety"
     ],
     "required": true
    },
    {
     "name": "severity",
     "type": "text",
     "enum": [
      "minor",
      "moderate",
      "major",
      "urgent"
     ]
    },
    {
     "name": "title",
     "type": "text",
     "required": true
    },
    {
     "name": "description",
     "type": "text"
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "open",
      "acknowledged",
      "in_repair",
      "resolved",
      "claimed",
      "waived"
     ]
    },
    {
     "name": "estimated_cost",
     "type": "numeric"
    },
    {
     "name": "resolved_at",
     "type": "timestamptz"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "media": {
   "layer": "operations",
   "note": "captured_at (device) stored separately from uploaded_at (server); the gap is a signal. sha256 catches re-submitted old photos.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "task_id",
     "type": "uuid",
     "references": "tasks"
    },
    {
     "name": "finding_id",
     "type": "uuid",
     "references": "findings"
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "kind",
     "type": "text",
     "enum": [
      "photo",
      "video"
     ],
     "required": true
    },
    {
     "name": "storage_uri",
     "type": "text",
     "required": true
    },
    {
     "name": "room",
     "type": "text"
    },
    {
     "name": "captured_at",
     "type": "timestamptz"
    },
    {
     "name": "uploaded_at",
     "type": "timestamptz"
    },
    {
     "name": "captured_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "sha256",
     "type": "text"
    }
   ]
  },
  "assets": {
   "layer": "assets",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "category",
     "type": "text",
     "enum": [
      "appliance",
      "hvac",
      "hot_tub",
      "pool",
      "furniture",
      "other"
     ],
     "required": true
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "make",
     "type": "text"
    },
    {
     "name": "model",
     "type": "text"
    },
    {
     "name": "serial_no",
     "type": "text"
    },
    {
     "name": "installed_on",
     "type": "date"
    },
    {
     "name": "warranty_until",
     "type": "date"
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "in_service",
      "needs_repair",
      "retired"
     ]
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "work_orders": {
   "layer": "assets",
   "note": "The money-and-labor half of a problem; the finding is the discovery half. Preventive work has no finding.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "unit_id",
     "type": "uuid",
     "references": "units",
     "required": true
    },
    {
     "name": "asset_id",
     "type": "uuid",
     "references": "assets"
    },
    {
     "name": "finding_id",
     "type": "uuid",
     "references": "findings"
    },
    {
     "name": "vendor_id",
     "type": "uuid",
     "references": "vendors"
    },
    {
     "name": "title",
     "type": "text",
     "required": true
    },
    {
     "name": "priority",
     "type": "text",
     "enum": [
      "low",
      "normal",
      "high",
      "emergency"
     ]
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "open",
      "scheduled",
      "in_progress",
      "done",
      "cancelled"
     ]
    },
    {
     "name": "scheduled_for",
     "type": "timestamptz"
    },
    {
     "name": "completed_at",
     "type": "timestamptz"
    },
    {
     "name": "cost",
     "type": "numeric"
    },
    {
     "name": "invoice_ref",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz"
    },
    {
     "name": "updated_at",
     "type": "timestamptz"
    }
   ]
  },
  "sync_events": {
   "layer": "integration",
   "note": "Append-only inbox: ingest first, process after; unique (source, external_event_id) makes retries harmless.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "source",
     "type": "text",
     "required": true
    },
    {
     "name": "event_type",
     "type": "text",
     "required": true
    },
    {
     "name": "external_event_id",
     "type": "text"
    },
    {
     "name": "payload",
     "type": "jsonb",
     "required": true
    },
    {
     "name": "received_at",
     "type": "timestamptz"
    },
    {
     "name": "processed_at",
     "type": "timestamptz"
    },
    {
     "name": "error",
     "type": "text"
    }
   ],
   "unique": [
    [
     "source",
     "external_event_id"
    ]
   ]
  },
  "audit_log": {
   "layer": "integration",
   "note": "Populated on every mutation of operational data; records attached to damage claims need an audit trail.",
   "columns": [
    {
     "name": "id",
     "type": "bigint",
     "primary_key": true
    },
    {
     "name": "actor_id",
     "type": "uuid",
     "note": "staff id, or null for system"
    },
    {
     "name": "entity",
     "type": "text",
     "required": true
    },
    {
     "name": "entity_id",
     "type": "uuid",
     "required": true
    },
    {
     "name": "action",
     "type": "text",
     "enum": [
      "create",
      "update",
      "status_change",
      "delete"
     ],
     "required": true
    },
    {
     "name": "changes",
     "type": "jsonb"
    },
    {
     "name": "at",
     "type": "timestamptz"
    }
   ]
  }
 },
 "deliberately_out_of_scope": [
  "Pricing, payouts, accounting (the PMS and accounting stack own money).",
  "Guest messaging (saturated commercial category; data lives with the channel).",
  "Automated condition analysis of media contents (a computer-vision problem, not a schema problem; RapidEye provides that layer commercially)."
 ]
}