Appearance
Properties & Rental History
Persistent property entities separate from listings — a physical unit that exists whether or not it's currently for rent.
Problem
Today, "property" and "listing" are the same thing. All physical attributes (address, bedrooms, parking) and temporal attributes (price, availability, lease type) live on one listings row. This creates several issues:
- Re-listing friction — when a landlord wants to re-list the same apartment, they create a new listing from scratch. Address, bedrooms, photos — all re-entered.
- Lost history — when a listing is archived, there's no record that this unit was ever for rent. Price history, tenant turnover, time-on-market — all gone.
- No portfolio view — landlords can only see active/draft/archived listings. There's no view of "my properties" including ones not currently listed.
- Scraper duplicates — the same apartment appears as separate, unrelated listings each time it's scraped across weeks or platforms.
- No property database — we can't build a comprehensive database of Montreal rental units because everything is tied to transient listings.
Entity Hierarchy
Building (optional, groups units at same address)
└── Property (persistent physical unit — apartment, condo, house)
└── Listing (time-bound rental offer, 0 or many per property)A property is a specific rentable unit: "Apt 4B at 123 Rue Saint-Denis". It persists regardless of whether it's currently listed. A listing is a rental advertisement: "Spacious 3½ in Plateau, $1,400/mo, available July 1st". Listings come and go. A property can have many listings over time, but typically only one active listing at a time.
What Belongs Where
| Property (persistent) | Listing (temporal) |
|---|---|
| Address, unit #, coordinates | Title, description |
| Neighborhood | Price per month |
| Property type (apartment/condo/house) | Lease type, available date |
| Bedrooms, bathrooms, area | Contact email/phone |
| Building features (parking, laundry) | Status (draft/active/rented/archived) |
| Pet policy, smoking policy | Published/archived dates |
| Landlord ownership | Rental tracking (tenant, actual rent, lease dates) |
| Property-level images (the unit itself) | Listing-specific marketing images |
| Notes (CRM, persistent across listings) | Inquiries (per listing campaign) |
Recommended Approach: Gradual Introduction
Rather than a big-bang refactor that touches every file, introduce properties incrementally in 4 steps.
Step 1: Thin properties table + FK on listings
Create a minimal properties table:
| Field | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| addressStreet | string | Street address |
| addressUnit | string | Unit/apartment number |
| addressCity | string | City |
| addressPostalCode | string | Postal code |
| latitude | decimal | Geolocation |
| longitude | decimal | Geolocation |
| neighborhoodId | uuid FK | Linked neighborhood |
| propertyType | enum | apartment, condo, house, etc. |
| bedrooms | integer | Number of bedrooms |
| bathrooms | integer | Number of bathrooms |
| areaSqFt | integer | Square footage |
| landlordId | uuid FK | Owner (user or scraper system user) |
| createdAt | timestamp | When the property was first seen |
Add property_id (nullable FK) to listings.
Migration: backfill properties from existing listings — group by address + unit hash, create one property per unique unit, link listings back.
Step 2: Landlord Portfolio View
New dashboard section: "My Properties"
- Lists all properties, including ones with no active listing
- Each property shows: address, unit, bedrooms, current listing status (or "Not listed")
- Property detail page: listing history timeline, notes, current status
- "Re-list" action: create new listing pre-filled from property data + previous listing details
- Notes move from listing-level to property-level (persistent across listing cycles)
Step 3: Scraper Integration
When importing scraped listings:
- Normalize the address (strip accents, standardize street types, etc.)
- Match against existing properties by normalized address + unit
- If match found: link new listing to existing property
- If no match: create new property, then link
This enables:
- Detecting when a previously-seen unit comes back on the market
- Cross-platform deduplication at the property level (Phase 13)
- Building a growing database of Montreal rental units
Step 4: Move Persistent Fields Up
Gradually migrate physical attributes from listings to properties:
- New listings inherit defaults from their property (bedrooms, parking, etc.)
- Listings can still override property-level attributes (e.g., after renovation)
- Images split: property gallery (the unit) vs. listing photos (staging/marketing)
- Reduces data duplication across listing cycles
Relationship to Other Phases
Building Profiles (Phase 11)
Properties are the foundation for Building Profiles. A building groups properties at the same street address (minus unit #). Building-level amenities (gym, pool, doorman) apply to all properties within. Building Profiles should be built on top of the property layer, not before it.
Market Analytics (Phase 12)
Property-level history directly enables:
- Price history per unit (not just per listing)
- Turnover rate tracking (how often does this unit go back on market?)
- Days-to-rent analysis per property
- Neighborhood price trends based on stable property data
Multi-Scraper Dedup (Phase 13)
Property matching is the natural deduplication layer. Instead of comparing listing text across platforms, match at the property level: same address + unit = same property, regardless of which platform it was scraped from.
Open Design Decisions
These need to be resolved during implementation:
Address normalization — Montreal addresses vary widely (123 Rue St-Denis vs 123 Saint-Denis St vs 123 rue Saint-Denis). Strategy: normalize on insert + fuzzy matching by lat/lng proximity + unit number.
Ownership — Can a property change owners? (e.g., property manager changes). Probably yes — support ownership transfer.
Scraped properties — Initially owned by the scraper system user. Claimable by landlords (extends the existing claims system).
Notes migration — Move notes from listing-level to property-level? Probably yes — notes are about the unit, not the listing campaign. But keep inquiry notes at listing level.
Image ownership — Property images (the physical unit) vs. listing images (marketing photos for this campaign). How to split? Start simple: all images stay on listings, add property-level gallery later.
One active listing rule — Should we enforce max one active listing per property? Probably yes, with a warning UI.