A vacation rental operations database needs 18 tables in 7 layers, and its pivot is the turnover: the window between the stay that just ended and the one about to begin. Reservations stay a read-only mirror of your PMS; turnovers generate tasks; tasks run against versioned SOP checklists; what they discover becomes findings, attributed to the departing stay and backed by condition media with capture times and content hashes. This page publishes the full schema in three formats, free to use and adapt.

Our July 2026 audit of 1,125 GitHub repositories found no maintained open-source data model for vacation rental operations. Open source solved the booking engine and the rent ledger; the operations layer, where turnovers, cleaners, inspections, and damage live, never got one. Meanwhile the operators we talk to every week are building internal tools with AI coding agents, and every one of those builds starts with the same question: what are the tables? This is our answer, drawn from what running condition documentation for professional operators has taught us the hard way. Use it as a starting point, delete what you do not need, and keep your PMS as the system of record.

Five decisions that make or break the design

Your PMS remains the system of record for bookings

The reservations table here is a one-way synced mirror, keyed by (source, external_id) so re-syncing is idempotent. Two systems that both think they own bookings will eventually disagree, and the PMS is the one connected to the channels. Sync in via API where you have one, or iCal feeds, which every major channel exports.

The turnover is the pivot entity

Not the reservation, not the task. The turnover links the departing stay to the arriving one, and that adjacency is what makes everything downstream work, especially attribution: what a turnover discovers is answerable by the stay that just ended.

Condition evidence is first-class, not an attachment

Media rows store the device capture time separately from the upload time (the gap between them is a signal) and a sha256 content hash, which deduplicates and catches the oldest trick in the book: re-submitting last week's photos for today's clean.

Checklists are versioned and immutable

Editing an SOP creates version + 1; completed work pins the version it was actually performed against. Without this, every SOP edit silently rewrites the history of work already done, and you can never again prove what was asked of a cleaner on a given day.

All timestamps are UTC, the property carries its timezone

Store timestamptz everywhere, keep an IANA timezone string on the property, and convert at render time. Operations tools live and die on "did this happen before checkout"; local-time storage makes that question unanswerable twice a year.

The model, layer by layer

Seven layers, 18 tables. Everything operational references units, never properties directly, so the same schema serves a one-unit beach house and a 40-unit aparthotel. The highlighted tables are the operational heart.

1 · Propertywhat you operate
propertiesunitslistings
2 · Bookingsynced mirror, PMS owns it
guestsreservations
3 · Peoplewho does the work
vendorsstaff
4 · SOPsversioned, immutable
checklistschecklist_items
5 · Operationsthe heart
turnoverstaskstask_item_resultsfindingsmedia
6 · Assetsthings that break
assetswork_orders
7 · Integrationthe outside world
sync_eventsaudit_log

The pivot, drawn out

reservationsreservation_outthe stay that just ended
the pivotturnovercheckout to next check-in
reservationsreservation_inthe stay about to begin
work performedtasksclean, inspection, maintenance
what they discoverfindingsattributed to reservation_out
the evidencemediacapture time + content hash

When a finding is created during a turnover, the departing reservation's id is copied into attributed_reservation_id, denormalized on purpose so the attribution survives even if the turnover row is later edited. That single column is the difference between "we found a broken hot tub cover at some point" and an evidence chain a damage claim can stand on.

The heart of it, in SQL

The three tables everything else orbits. The full DDL for all 18 is in schema.sql below.

schema.sql (excerpt)
CREATE TABLE turnovers (
  id                 uuid PRIMARY KEY,
  unit_id            uuid NOT NULL REFERENCES units(id),
  reservation_out_id uuid REFERENCES reservations(id),  -- the departing stay
  reservation_in_id  uuid REFERENCES reservations(id),  -- the arriving stay
  window_start       timestamptz,
  window_end         timestamptz,
  status             text NOT NULL DEFAULT 'pending'
);

CREATE TABLE findings (
  id                        uuid PRIMARY KEY,
  unit_id                   uuid NOT NULL REFERENCES units(id),
  task_id                   uuid REFERENCES tasks(id),
  attributed_reservation_id uuid REFERENCES reservations(id),
  category    text NOT NULL,  -- damage | cleanliness | maintenance | missing_item | safety
  severity    text NOT NULL DEFAULT 'minor',
  title       text NOT NULL,
  status      text NOT NULL DEFAULT 'open',
  estimated_cost numeric(10,2)
);

CREATE TABLE media (
  id           uuid PRIMARY KEY,
  task_id      uuid REFERENCES tasks(id),
  finding_id   uuid REFERENCES findings(id),
  unit_id      uuid NOT NULL REFERENCES units(id),
  kind         text NOT NULL,        -- photo | video
  storage_uri  text NOT NULL,
  captured_at  timestamptz,           -- device clock; gap vs uploaded_at is a signal
  uploaded_at  timestamptz NOT NULL,
  sha256       text                   -- dedup + catches re-submitted old photos
);

Get the schema

Three formats at stable URLs, free to use, adapt, and build on, with or without attribution. If you are building with an AI coding agent, give it the Markdown or JSON URL; both were written to be read by machines as much as by people.

bash
# fetch the schema in your format of choice
curl -O https://rapideyeinspections.com/blog/str-operations-data-model/schema.sql
curl -O https://rapideyeinspections.com/blog/str-operations-data-model/schema.md
curl -O https://rapideyeinspections.com/blog/str-operations-data-model/schema.json

What the model deliberately leaves out

Three things are missing on purpose, and the reasons matter more than the omissions:

  • Pricing, payouts, and accounting. Your PMS and accounting stack own money. Mirroring financials into an internal ops tool doubles your reconciliation surface for zero operational gain.
  • Guest messaging. A saturated commercial category where the data lives with the channel. If you need it, buy it; our guest messaging comparison covers the field.
  • Automated condition analysis. Detecting damage or verifying work from what is inside the media table is a computer-vision problem, not a schema problem. The schema's job is to guarantee the evidence exists, is attributed to a stay, and can be trusted. The analysis layer on top is what RapidEye does commercially, on exactly this kind of evidence trail.

The model also stops short of prescribing an application architecture. For the decision that comes before this page (whether to build at all, and which layers build well with AI agents versus which break), read Should You Build Your Own Vacation Rental Ops Dashboard With AI?


Quick FAQ

What tables does a vacation rental operations database need?

Eighteen tables in seven layers cover the core: properties, units, and listings (property layer); guests and reservations (a read-only mirror of your PMS); vendors and staff; versioned checklists and checklist items (SOPs); turnovers, tasks, task item results, findings, and media (operations); assets and work orders (maintenance); and sync_events plus audit_log (integration). The full schema is downloadable free from this page.

Should reservations live in my internal tool or my PMS?

Your PMS stays the system of record. The internal tool keeps a one-way synced mirror of reservations, keyed by source and external_id so repeated syncs are idempotent. Never write bookings into the internal tool first: two systems that both think they own bookings will eventually disagree, and the PMS is the one connected to the channels.

How do you attribute damage to a specific guest stay in the data model?

Through the turnover. A turnover row links the reservation that just ended (reservation_out_id) and the one about to begin. When a finding is created during a turnover, the model copies the departing reservation's id into the finding's attributed_reservation_id. Anything discovered in that window is answerable by the stay that just ended, which is exactly the evidence chain a damage claim needs.

What formats is the schema available in?

Three: schema.sql (PostgreSQL DDL, ready to run), schema.json (a machine-readable entity map), and schema.md (a Markdown reference that documents every table and column). All three live at stable URLs under this page and are free to use, adapt, and build on, with or without attribution.

Keep reading