# The Long-Term Rental Data Model (reference schema v0.1)

Source: https://rapideyeinspections.com/blog/rental-property-data-model/
Also available: [schema.sql](https://rapideyeinspections.com/blog/rental-property-data-model/schema.sql) (PostgreSQL DDL, executed and idempotency-tested against PostgreSQL 14) and [schema.json](https://rapideyeinspections.com/blog/rental-property-data-model/schema.json) (machine-readable entity map).
Companion models: [The Hotel Operations Data Model](https://rapideyeinspections.com/blog/hotel-operations-data-model/) and [The STR Operations Data Model](https://rapideyeinspections.com/blog/str-operations-data-model/).
Published by RapidEye Research, August 2026. Free to use, adapt, and build on, with or without attribution.

A reference data model for building internal long-term rental operations tools: properties, units, and owners; tenants, applications, and leases; the append-only rent ledger; move-in and move-out inspections as paired checklists; the security deposit and its documentation-backed deductions; the unit turn lifecycle; and maintenance. 22 tables in 7 layers.

## Design principles

1. **The deposit clock is law, and it varies by state.** Return deadlines and their statutes live in the seeded state_deposit_rule table as data, not as constants in application code. California gives 21 days (Civil Code 1950.5), Texas 30 (Property Code 92.103), New York 14 (General Obligations Law 7-108).
2. **A deduction is only as strong as its documentation.** Deposit_deduction.inspection_item_id is NOT NULL, so an undocumented deduction cannot be inserted. Unpaid rent settles through the rent ledger (entry_type deposit_applied), never as a condition deduction.
3. **Move-in and move-out are the same checklist run twice.** Inspection items are keyed (room_label, item) so the move-out row joins to its move-in twin, and the move-out inspection carries baseline_inspection_id. Damage is a delta between two documented states.
4. **Turn status is a projection of milestone dates, never a typed field.** Unit_turn.status is a generated column, and the legal lifecycle edges are seeded in unit_turn_transition for one-lookup validation.
5. **The rent ledger is append-only.** Corrections are adjustment rows, never updates; the balance of a lease is SUM(amount).
6. **Evidence beats adjectives.** An inspection item rated damaged or missing with zero photos is rejected by CHECK.

## The turn state machine

`unit_turn.status` is a stored generated column, never written by hand. unit_turn.status is GENERATED from milestone columns: new_lease_id set means leased; else market_ready_on set means market_ready; else make_ready_started_on set means make_ready; else moved_out_on set means move_out_inspection; else notice_given.

| State | Meaning |
|---|---|
| notice_given | Notice received; tenant still in possession. Every turn starts here. |
| move_out_inspection | Tenant vacated; the move-out inspection runs against the move-in baseline. |
| make_ready | Findings split into deposit deductions and the make-ready scope; work in progress. |
| market_ready | Punch list complete, final walk passed; unit is showable and leasable. |
| leased | New lease signed and unit handed over; the turn closes. |

Legal transitions (seeded into `unit_turn_transition`, so an application or agent can validate a proposed move with one lookup):

| From | To | Trigger |
|---|---|---|
| notice_given | move_out_inspection | tenant vacated and returned keys; the move-out inspection runs against the move-in baseline |
| move_out_inspection | make_ready | move-out inspection completed; findings split into deposit deductions and the make-ready scope |
| make_ready | market_ready | make-ready tasks complete and the final walk passed |
| market_ready | make_ready | a showing or pre-move-in walk surfaced new work; the unit goes back into make-ready |
| market_ready | leased | new lease signed and the unit handed over; the turn closes |

The CHECK constraints on `unit_turn` enforce the same ordering structurally: `make_ready_started_on` requires `moved_out_on`, `market_ready_on` requires `make_ready_started_on`, and `new_lease_id` requires `market_ready_on`.

## The deposit clock, as seeded data

`state_deposit_rule` ships with eight states, each verified against the named statute on 2026-08-01. Query it, do not hardcode it; add your own states the same way.

| State | Return deadline (days) | Claim-notice deadline | Deposit cap | Statute |
|---|---|---|---|---|
| CA | 21 |  | One month's rent; small landlords meeting the statute's criteria may collect up to two months. | Cal. Civ. Code 1950.5 |
| TX | 30 |  | No statutory cap. | Tex. Prop. Code 92.103 |
| NY | 14 |  | One month's rent. | N.Y. Gen. Oblig. Law 7-108 |
| FL | 15 | 30 | No statutory cap. | Fla. Stat. 83.49(3) |
| WA | 30 |  | No general statutory cap. | RCW 59.18.280 |
| AZ | 14 |  | One and one-half months' rent. | A.R.S. 33-1321 |
| MA | 30 |  | First month's rent. | M.G.L. c. 186, s. 15B |
| CO | 30 |  | No general statutory cap. | C.R.S. 38-12-103 |

Notes: Florida runs two clocks (return within 15 days with no claim; written notice of a claim within 30). Arizona counts 14 days excluding weekends and legal holidays. Colorado allows the lease to extend its one-month deadline to at most 60 days. California requires receipts with the itemized statement when deductions exceed $125, and photographs at move-in and move-out for tenancies beginning July 1, 2025 or later.

## Layer 1: Portfolio (4 tables)

What you manage, and for whom. The unit is the atom everything operational references; a single-family home is a property with exactly one unit.

### property_owner

The owner of record. Third-party managers answer to one: statements, disbursements, and turn costs roll up here.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| name | text | required |
| owner_type | text | required; default individual; one of: individual, llc, trust, corporation, partnership, other |
| email | text |  |
| phone | text |  |
| created_at | timestamptz | required |

### property

A building or home. state_code is the join key into state_deposit_rule, where the deposit-return clock for every lease at this property lives.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| property_owner_id | uuid | FK to `property_owner.id` |
| name | text | required |
| address_line1 | text | required |
| address_line2 | text |  |
| city | text |  |
| state_code | text | required; USPS code; joins to state_deposit_rule |
| postal_code | text |  |
| property_type | text | required; default single_family; one of: single_family, condo, townhome, duplex_to_fourplex, small_multifamily, large_multifamily, mixed_use |
| year_built | integer |  |
| created_at | timestamptz | required |
| updated_at | timestamptz | required |

Constraints: `CHECK ((char_length(state_code) = 2))`

### unit

The rentable unit, the atom of the model. Leases, turns, inspections, and maintenance all key off it.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| property_id | uuid | required; FK to `property.id` |
| unit_number | text | required; text, not int: '2B', 'GARDEN', 'ADU' |
| bedrooms | numeric(3,1) |  |
| bathrooms | numeric(3,1) |  |
| square_feet | integer |  |
| market_rent | numeric(10,2) | asking rent when vacant; actual rent lives on lease |
| created_at | timestamptz | required |
| updated_at | timestamptz | required |

Constraints: `UNIQUE (property_id, unit_number)`

### appliance

Appliances get identity because they get inspected, break, and carry warranties. Inspection items and work orders can name the exact appliance.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_id | uuid | required; FK to `unit.id` |
| category | text | required; one of: refrigerator, range_oven, dishwasher, microwave, washer, dryer, water_heater, hvac, garbage_disposal, other |
| brand | text |  |
| model | text |  |
| serial_number | text |  |
| installed_on | date |  |
| warranty_expires_on | date |  |
| created_at | timestamptz | required |

## Layer 2: People (4 tables)

Who works the portfolio and who wants in. Tenants carry minimal PII on purpose; applications are their own lifecycle with the decision and reason preserved.

### staff

The people who run the portfolio: managers, leasing agents, techs, inspectors.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| full_name | text | required |
| role | text | required; one of: property_manager, leasing_agent, maintenance_tech, inspector, accountant, other |
| email | text |  |
| active | boolean | required; default true |
| created_at | timestamptz | required |

### vendor

Outside trades. Tracks certificate-of-insurance expiry and whether a W-9 is on file.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| name | text | required |
| trade | text | required; default general; one of: cleaning, painting, flooring, plumbing, electrical, hvac, appliance, landscaping, locksmith, pest_control, roofing, general, other |
| email | text |  |
| phone | text |  |
| insurance_expires_on | date | COI expiry; schedule nothing past it |
| w9_on_file | boolean | required; every vendor paid over the reporting threshold needs a 1099 |
| active | boolean | required; default true |
| created_at | timestamptz | required |

### tenant

A person who rents. Minimal PII on purpose; screening data stays in the screening vendor’s system.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| full_name | text | required |
| email | text |  |
| phone | text |  |
| created_at | timestamptz | required |

### rental_application

The application lifecycle, separate from tenant. Keeps the decision, the timing, and the denial reason; lease_id set on conversion.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_id | uuid | required; FK to `unit.id` |
| applicant_name | text | required |
| email | text |  |
| phone | text |  |
| desired_move_in | date |  |
| monthly_income | numeric(10,2) |  |
| status | text | required; default received; one of: received, screening, approved, denied, withdrawn, converted |
| screening_report_at | timestamptz |  |
| income_verified | boolean | required; default false |
| denial_reason | text | required in practice when status = 'denied' |
| decided_at | timestamptz |  |
| lease_id | uuid | FK to `lease.id`; set on conversion |
| created_at | timestamptz | required |

## Layer 3: Leases and money (4 tables)

The tenancy and its append-only ledger. Renewals are new lease rows chained by previous_lease_id; ledger corrections are adjustment rows, never updates; notices are dated legal facts.

### lease

The tenancy. A renewal is a new row pointing at its predecessor via previous_lease_id, never an edit.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_id | uuid | required; FK to `unit.id` |
| previous_lease_id | uuid | FK to `lease.id`; renewal chain: a renewal is a new row, never an edit |
| lease_type | text | required; default fixed_term; one of: fixed_term, month_to_month |
| start_date | date | required |
| end_date | date |  |
| rent_amount | numeric(10,2) | required |
| rent_due_day | integer | required; default 1 |
| late_fee_grace_days | integer | statutory floors exist, e.g. Colorado requires rent at least 7 days late (C.R.S. 38-12-105) |
| late_fee_amount | numeric(10,2) | caps exist, e.g. Colorado: greater of $50 or 5% of past-due amount |
| status | text | required; default draft; one of: draft, active, ended |
| signed_at | timestamptz |  |
| moved_in_on | date |  |
| moved_out_on | date |  |
| created_at | timestamptz | required |
| updated_at | timestamptz | required |

Constraints: `CHECK (((rent_due_day >= 1) AND (rent_due_day <= 28)))`; `CHECK ((rent_amount > (0)::numeric))`; `CHECK (((end_date IS NULL) OR (end_date > start_date)))`; `CHECK (((lease_type = 'month_to_month'::text) OR (end_date IS NOT NULL)))`

### lease_tenant

Many-to-many people-on-lease with roles: primary, co-tenant, occupant, guarantor.

| column | type | notes |
|---|---|---|
| lease_id | uuid | required; FK to `lease.id` |
| tenant_id | uuid | required; FK to `tenant.id` |
| role | text | required; default primary; one of: primary, co_tenant, occupant, guarantor |

### rent_ledger_entry

Append-only money history for a lease. Charges positive, money in negative, enforced by CHECK; balance is SUM(amount). deposit_applied is how unpaid rent meets the deposit.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| lease_id | uuid | required; FK to `lease.id` |
| entry_type | text | required; deposit_applied is how unpaid rent meets the deposit; it never appears in deposit_deduction; one of: rent_charge, other_charge, late_fee, payment, credit, deposit_applied, adjustment |
| amount | numeric(10,2) | required; charges positive, money in negative; enforced by CHECK |
| effective_on | date | required |
| method | text | one of: ach, check, card, cash, money_order, other |
| memo | text |  |
| created_by | uuid | FK to `staff.id` |
| created_at | timestamptz | required |

Constraints: `CHECK ((amount <> (0)::numeric))`

### notice

Dated legal facts: notice to vacate, non-renewal, violations, entry, rent increases. served_on starts clocks.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| lease_id | uuid | required; FK to `lease.id` |
| notice_type | text | required; one of: tenant_notice_to_vacate, landlord_non_renewal, lease_violation, entry_notice, rent_increase, termination |
| served_on | date | required; starts the legal clocks |
| effective_on | date |  |
| delivery_method | text | one of: hand, mail, certified_mail, email, posting |
| document_uri | text |  |
| note | text |  |
| created_at | timestamptz | required |

Constraints: `CHECK (((effective_on IS NULL) OR (effective_on >= served_on)))`

## Layer 4: Inspections (2 tables)

The same checklist, run twice. A move-out inspection points at its move-in baseline; items are keyed (room_label, item) so the two runs join row for row; damaged or missing items require photos by CHECK.

### inspection

A documented walkthrough: move_in, move_out, or routine. A move_out points at its move_in via baseline_inspection_id so damage is a delta between two documented states.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_id | uuid | required; FK to `unit.id` |
| lease_id | uuid | FK to `lease.id` |
| inspection_type | text | required; one of: move_in, move_out, routine |
| baseline_inspection_id | uuid | FK to `inspection.id`; the move_in this one is compared against |
| inspected_at | timestamptz | required |
| inspected_by | uuid | FK to `staff.id` |
| tenant_present | boolean | several statutes give the tenant a right to be there |
| summary | text |  |
| created_at | timestamptz | required |

### inspection_item

One checklist line: room, item, condition, evidence. UNIQUE (inspection_id, room_label, item) is the pairing mechanism; damaged or missing requires photo_count > 0.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| inspection_id | uuid | required; FK to `inspection.id` |
| room_label | text | required; 'bedroom_1', 'kitchen', 'exterior' |
| item | text | required; 'carpet', 'north_wall', 'refrigerator' |
| condition_rating | text | required; one of: new, good, fair, worn, damaged, missing |
| notes | text |  |
| photo_count | integer | required; damaged or missing requires photo_count > 0 by CHECK |
| appliance_id | uuid | FK to `appliance.id`; when the item IS an appliance, say which one |
| created_at | timestamptz | required |

Constraints: `UNIQUE (inspection_id, room_label, item)`; `CHECK ((photo_count >= 0))`; `CHECK (((condition_rating <> ALL (ARRAY['damaged'::text, 'missing'::text])) OR (photo_count > 0)))`

## Layer 5: The deposit (3 tables)

A clock, a ledger, and documentation. Statutory return deadlines are seeded data in state_deposit_rule; every deduction must reference the inspection_item that documents it; unpaid rent settles in the rent ledger, never as a deduction.

### state_deposit_rule

The deposit-return clock as seeded, verified data: deadline days, cap, documentation requirements, and the statute, per state.

| column | type | notes |
|---|---|---|
| state_code | text | required |
| return_deadline_days | integer | required |
| claim_notice_deadline_days | integer | for two-clock states like Florida; null where one clock covers both |
| deadline_note | text |  |
| deposit_cap | text |  |
| documentation_note | text |  |
| statute | text | required |
| verified_on | date | required |

Constraints: `CHECK ((char_length(state_code) = 2))`

### security_deposit

One deposit per lease, enforced UNIQUE. status is generated from the return facts; disposition_sent_on is what the statutory clock judges.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| lease_id | uuid | required; FK to `lease.id` |
| amount | numeric(10,2) | required |
| collected_on | date |  |
| held_in | text |  |
| disposition_sent_on | date | the itemized statement date; what the statutory clock judges |
| returned_on | date |  |
| amount_returned | numeric(10,2) |  |
| status | text | GENERATED STORED; generated: held / returned_in_full / partially_withheld / fully_withheld |
| created_at | timestamptz | required |

Constraints: `UNIQUE (lease_id)`; `CHECK ((amount >= (0)::numeric))`; `CHECK (((returned_on IS NULL) OR (amount_returned IS NOT NULL)))`; `CHECK (((amount_returned IS NULL) OR ((amount_returned >= (0)::numeric) AND (amount_returned <= amount))))`

### deposit_deduction

A charge against the deposit. inspection_item_id is NOT NULL by design: no documented condition finding, no deduction. Unpaid rent never appears here.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| security_deposit_id | uuid | required; FK to `security_deposit.id` |
| inspection_item_id | uuid | required; FK to `inspection_item.id`; NOT NULL by design: no documented finding, no deduction |
| category | text | required; one of: damage, cleaning_beyond_normal, missing_item, unreturned_keys, other |
| amount | numeric(10,2) | required |
| description | text | required |
| vendor_id | uuid | FK to `vendor.id` |
| receipt_uri | text | invoice, estimate, or receipt backing the amount |
| created_at | timestamptz | required |

Constraints: `CHECK ((amount > (0)::numeric))`

## Layer 6: The turn (3 tables)

Notice to new lease as a state machine. unit_turn.status is a generated projection of milestone dates; the legal edges are seeded in unit_turn_transition; tasks carry their provenance back to the inspection finding that raised them.

### unit_turn

The make-ready lifecycle from notice to new lease. status is a generated projection of milestone dates; CHECKs force the milestones to arrive in order.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_id | uuid | required; FK to `unit.id` |
| vacating_lease_id | uuid | required; FK to `lease.id` |
| notice_id | uuid | FK to `notice.id` |
| new_lease_id | uuid | FK to `lease.id`; set when the unit is handed over; closes the turn |
| notice_received_on | date | required |
| expected_move_out_on | date |  |
| moved_out_on | date |  |
| move_out_inspection_id | uuid | FK to `inspection.id` |
| make_ready_started_on | date |  |
| market_ready_on | date |  |
| status | text | GENERATED STORED; generated projection of milestone dates; see state machine |
| created_at | timestamptz | required |
| updated_at | timestamptz | required |

Constraints: `CHECK (((make_ready_started_on IS NULL) OR (moved_out_on IS NOT NULL)))`; `CHECK (((moved_out_on IS NULL) OR (moved_out_on >= notice_received_on)))`; `CHECK (((market_ready_on IS NULL) OR (make_ready_started_on IS NOT NULL)))`; `CHECK (((market_ready_on IS NULL) OR (make_ready_started_on IS NULL) OR (market_ready_on >= make_ready_started_on)))`; `CHECK (((new_lease_id IS NULL) OR (market_ready_on IS NOT NULL)))`

### unit_turn_transition

The legal edges of the turn lifecycle, seeded, so an application or agent validates a proposed move with one lookup.

| column | type | notes |
|---|---|---|
| from_status | text | required; one of: notice_given, move_out_inspection, make_ready, market_ready, leased |
| to_status | text | required; one of: notice_given, move_out_inspection, make_ready, market_ready, leased |
| trigger_event | text | required |

### unit_turn_task

Sequenced work within a turn. source plus inspection_item_id answer who pays: tenant-charged damage or owner-funded refresh.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_turn_id | uuid | required; FK to `unit_turn.id` |
| seq | integer | required |
| task_type | text | required; one of: cleaning, painting, flooring, repair, appliance, landscaping, rekey, punch_list, final_walk, other |
| title | text | required |
| source | text | required; tasks with source 'move_out_inspection' carry the inspection_item that raised them; one of: move_out_inspection, standard_turn, ad_hoc |
| inspection_item_id | uuid | FK to `inspection_item.id` |
| vendor_id | uuid | FK to `vendor.id` |
| assigned_staff_id | uuid | FK to `staff.id` |
| status | text | required; default pending; one of: pending, scheduled, in_progress, done, verified, skipped |
| scheduled_for | date |  |
| completed_at | timestamptz |  |
| actual_cost | numeric(10,2) |  |
| note | text |  |
| created_at | timestamptz | required |

Constraints: `UNIQUE (unit_turn_id, seq)`

## Layer 7: Maintenance (2 tables)

What breaks between turns. Requests are triage; work orders are dispatch; source records whether work was reactive, found by inspection, or part of a turn.

### maintenance_request

The tenant-reported issue and its triage: priority, habitability flag, entry permission.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| unit_id | uuid | required; FK to `unit.id` |
| lease_id | uuid | FK to `lease.id` |
| reported_by_tenant_id | uuid | FK to `tenant.id` |
| channel | text | required; default portal; one of: portal, phone, email, text, inspection, staff |
| category | text | required; default other; one of: plumbing, electrical, hvac, appliance, pest, structural, safety, cosmetic, other |
| priority | text | required; default routine; one of: emergency, urgent, routine |
| is_habitability | boolean | required; habitability issues run on legal clocks cosmetic ones do not |
| entry_permission | boolean | did the tenant authorize entry without them present |
| description | text | required |
| status | text | required; default open; one of: open, triaged, scheduled, in_progress, completed, verified, cancelled |
| reported_at | timestamptz | required |
| completed_at | timestamptz |  |
| created_at | timestamptz | required |

### work_order

Dispatched work with provenance: from a request, an inspection finding, a turn task, or preventive. Costs and invoice attach here.

| column | type | notes |
|---|---|---|
| id | uuid |  |
| property_id | uuid | required; FK to `property.id` |
| unit_id | uuid | FK to `unit.id`; null for property-level work (roof, common area) |
| source | text | required; default ad_hoc; one of: maintenance_request, routine_inspection, move_out_inspection, unit_turn, preventive, ad_hoc |
| maintenance_request_id | uuid | FK to `maintenance_request.id` |
| inspection_item_id | uuid | FK to `inspection_item.id` |
| unit_turn_task_id | uuid | FK to `unit_turn_task.id` |
| appliance_id | uuid | FK to `appliance.id` |
| vendor_id | uuid | FK to `vendor.id` |
| assigned_staff_id | uuid | FK to `staff.id` |
| priority | text | required; default routine; one of: emergency, urgent, routine, low |
| status | text | required; default open; one of: open, assigned, scheduled, in_progress, on_hold, completed, verified, cancelled |
| title | text | required |
| description | text |  |
| estimate_amount | numeric(10,2) |  |
| invoice_amount | numeric(10,2) |  |
| invoice_uri | text |  |
| scheduled_for | date |  |
| completed_at | timestamptz |  |
| created_at | timestamptz | required |
| updated_at | timestamptz | required |

## The queries the model was shaped around

Move-out vs move-in, side by side, for one turn:

```sql
SELECT mo.room_label, mo.item,
       mi.condition_rating AS at_move_in,
       mo.condition_rating AS at_move_out,
       mo.photo_count      AS move_out_photos
FROM inspection_item mo
JOIN inspection i        ON i.id = mo.inspection_id
JOIN inspection_item mi  ON mi.inspection_id = i.baseline_inspection_id
                        AND mi.room_label = mo.room_label
                        AND mi.item       = mo.item
WHERE i.id = :move_out_inspection_id
  AND mo.condition_rating IS DISTINCT FROM mi.condition_rating;
```

Every deduction on a deposit, with its documentation, ready for the itemized statement:

```sql
SELECT dd.description, dd.amount, dd.category,
       ii.room_label, ii.item, ii.condition_rating, ii.photo_count,
       dd.receipt_uri
FROM deposit_deduction dd
JOIN inspection_item ii ON ii.id = dd.inspection_item_id
WHERE dd.security_deposit_id = :deposit_id
ORDER BY ii.room_label, ii.item;
```

Days left on the deposit clock for every recently ended lease:

```sql
SELECT l.id AS lease_id, p.state_code, r.return_deadline_days,
       l.moved_out_on + r.return_deadline_days AS statement_due_on,
       (l.moved_out_on + r.return_deadline_days) - CURRENT_DATE AS days_left
FROM lease l
JOIN unit u  ON u.id = l.unit_id
JOIN property p ON p.id = u.property_id
JOIN state_deposit_rule r ON r.state_code = p.state_code
JOIN security_deposit sd ON sd.lease_id = l.id
WHERE l.moved_out_on IS NOT NULL AND sd.disposition_sent_on IS NULL;
```

## What the model deliberately leaves out

- **Accounting beyond the lease ledger.** Owner statements, trust accounting, and 1099 filing belong to the accounting stack; the ledger here is the operational truth a statement is built from.
- **Listings and syndication.** Market_ready is where this model hands off to whatever advertises the unit.
- **Screening data.** The application row records that screening happened and what was decided, never the report itself.
- **Automated condition analysis.** Whether the move-out photo of bedroom_1 carpet actually shows a burn is a computer-vision problem, not a schema problem. The schema makes sure the evidence exists, is paired against a baseline, and is attached to the deduction that charges for it. The analysis layer on top is what [RapidEye](https://rapideyeinspections.com/) does commercially.

