Skip to content

Schema Reference

Source: packages/database/src/schema.ts

Enums

user_role

tenant | landlord

property_type

apartment | condo | house | townhouse | studio | loft | basement | duplex | triplex

listing_status

draft | active | rented | archived

lease_type

fixed | month_to_month | sublet

inquiry_status

new | contacted | resolved | spam

claim_status

pending | approved | rejected

report_reason

already_rented | wrong_info | spam | duplicate | other

report_status

pending | reviewed | dismissed

content_status

draft | published | archived

Tables

users

ColumnTypeNotes
iduuidPK, auto-generated
emailtextunique, not null
password_hashtextbcrypt hash
nametext
phonetext
roleuser_roledefault: tenant
avatar_urltext
created_attimestamptz
updated_attimestamptz

neighborhoods

ColumnTypeNotes
iduuidPK
nametextunique, e.g. "Mile End"
slugtextunique, e.g. "mile-end"
boroughtexte.g. "Le Plateau-Mont-Royal"
latitudenumeric(10,7)center point
longitudenumeric(10,7)center point
description_entextnullable, markdown
description_frtextnullable, markdown
highlights_entextnullable, markdown
highlights_frtextnullable
seo_title_entextnullable
seo_title_frtextnullable
seo_description_entextnullable
seo_description_frtextnullable
cover_image_urltextnullable
walkability_scoreintegernullable, 0-100
transit_scoreintegernullable, 0-100
created_attimestamptz

Seeded with ~85 Montreal neighborhoods organized by borough.

neighborhood_stats_snapshots

Historical daily stats snapshots for price trends per neighborhood.

ColumnTypeNotes
iduuidPK
neighborhood_iduuidFK → neighborhoods, cascade delete
datetimestamptzsnapshot date
bedroomsintegernullable: null=overall, 0=studio, 1-3=specific, 4=4+
avg_priceintegeraverage price in cents
listing_countintegernot null, default: 0
min_priceinteger
max_priceinteger
created_attimestamptz

Unique index on (neighborhood_id, date, bedrooms). Populated daily at 2:00 AM by the scraper stats-snapshot job.

listings

ColumnTypeNotes
iduuidPK
landlord_iduuidFK → users, cascade delete
neighborhood_iduuidFK → neighborhoods, set null
titletextnot null
descriptiontextnot null
property_typeproperty_typenot null
statuslisting_statusdefault: draft
price_per_monthintegerin cents (150000 = $1,500)
lease_typelease_typedefault: fixed
bedroomsinteger0 = studio
bathroomsintegerdefault: 1
area_sq_ftintegeroptional
address_streettextnot null
address_unittextoptional
address_citytextdefault: Montreal
address_provincetextdefault: QC
address_postal_codetextnot null
latitudenumeric(10,7)for map
longitudenumeric(10,7)for map
amenitiesjsonbstring array
available_datetimestamptz
contact_emailtext
contact_phonetext
is_pet_friendlybooleandefault: false
is_smoking_allowedbooleandefault: false
has_parkingbooleandefault: false
has_laundrybooleandefault: false
viewsintegerdefault: 0, incremented on detail page view
published_attimestamptz
claimed_byuuidFK → users, set null. Claimed listings are never overwritten by scraper updates
claimed_attimestamptzset when claim approved
archived_attimestamptznullable, when archived. Cleared if listing is re-scraped (reactivated)
archive_reasontextnullable, e.g. "stale", "bulk-admin"
rental_priceintegernullable, actual rent agreed in cents
tenant_nametextnullable, who rented it
lease_start_datetimestamptznullable, lease start
lease_end_datetimestamptznullable, lease end
created_attimestamptz
updated_attimestamptz

listing_images

ColumnTypeNotes
iduuidPK
listing_iduuidFK → listings, cascade delete
urltextnot null
alt_texttext
sort_orderintegerdefault: 0
is_primarybooleandefault: false
created_attimestamptz

favorites

ColumnTypeNotes
iduuidPK
user_iduuidFK → users, cascade delete
listing_iduuidFK → listings, cascade delete
created_attimestamptz

Unique index on (user_id, listing_id).

inquiries

ColumnTypeNotes
iduuidPK
listing_iduuidFK → listings, cascade delete
user_iduuidFK → users, nullable, set null
nametextnot null
emailtextnot null
phonetext
messagetextnot null
statusinquiry_statusdefault: new
admin_notestextinternal admin notes
metadatajsonbsubmitter info: ip, userAgent, deviceType, browser, os, locale, referer
created_attimestamptz
updated_attimestamptz

listing_claims

ColumnTypeNotes
iduuidPK
listing_iduuidFK → listings, cascade delete
user_iduuidFK → users, cascade delete
statusclaim_statusdefault: pending
justificationtextnot null
admin_notestext
reviewed_attimestamptz
created_attimestamptz
updated_attimestamptz

Unique index on (listing_id, user_id).

listing_reports

User-submitted reports/flags on listings (spam, outdated, duplicate, etc).

ColumnTypeNotes
iduuidPK
listing_iduuidFK → listings, cascade delete
user_iduuidFK → users, set null on delete (nullable)
reasonreport_reasonnot null
commenttextoptional details
reporter_iptextIP for rate limiting
statusreport_statusdefault: pending
admin_notestextinternal admin notes
metadatajsonbuserAgent, deviceType, browser, os, locale
created_attimestamptz
resolved_attimestamptzset when reviewed/dismissed

listing_notes

ColumnTypeNotes
iduuidPK
listing_iduuidFK → listings, cascade delete
user_iduuidFK → users, cascade delete
contenttextnot null
created_attimestamptz
updated_attimestamptz

articles

Blog posts and wiki/guide pages with bilingual content.

ColumnTypeNotes
iduuidPK
typetextblog or wiki
slugtextunique, URL-safe
categorytextnullable, e.g. "tenant-rights"
title_entextnot null
title_frtextnot null
content_entextnot null, markdown
content_frtextnot null, markdown
excerpt_entextnullable, short summary
excerpt_frtextnullable
seo_title_entextnullable
seo_title_frtextnullable
seo_description_entextnullable
seo_description_frtextnullable
cover_image_urltextnullable
statuscontent_statusdefault: draft
sort_orderintegerdefault: 0, for wiki ordering
published_attimestamptznullable
created_attimestamptz
updated_attimestamptz

postal_areas

Forward Sortation Areas (FSA — 3-character postal code prefixes) for Greater Montreal with boundary polygons.

ColumnTypeNotes
iduuidPK
fsatextunique, not null (e.g., H2T)
nametextnot null, area name
provincetextnot null (QC)
latitudenumeric(10,7)centroid latitude
longitudenumeric(10,7)centroid longitude
boundaryjsonbGeoJSON polygon from Statistics Canada
neighborhood_iduuidFK → neighborhoods (nearest match)
median_household_incomeintegernullable, from 2021 Census
average_household_incomeintegernullable, from 2021 Census
created_attimestamptz

193 FSAs (123 H-prefix Montreal/Laval + 70 J-prefix South Shore/suburbs). 187 have boundary polygons.

postal_codes

Full 6-digit postal codes for Greater Montreal, linked to parent FSAs.

ColumnTypeNotes
iduuidPK
codetextunique, not null, normalized no space (e.g., H2T2Y2)
fsatextnot null, parent FSA (e.g., H2T)
ldutextnot null, Local Delivery Unit (e.g., 2Y2)
nametextcity/area name from GeoNames
latitudenumeric(10,7)centroid latitude
longitudenumeric(10,7)centroid longitude
boundaryjsonbReserved for future LDU boundaries
postal_area_iduuidFK → postal_areas
created_attimestamptz

~101K postal codes seeded from GeoNames (CC BY 4.0): 56K H-prefix + 45K J-prefix within ~50km of downtown Montreal.

census_tracts

Census tract boundaries and household income data from the 2021 Canadian Census (Statistics Canada). Used for the income choropleth map.

ColumnTypeNotes
iduuidPK
ctuidtextunique, not null. Census tract unique ID (e.g., "4620001.00")
cma_uidtextCMA code (462 = Montreal)
nametextshort tract name (e.g., "0001.00")
boundaryjsonbGeoJSON polygon from StatCan shapefile
median_household_incomeintegernullable, 2021 Census
average_household_incomeintegernullable, 2021 Census
median_after_tax_incomeintegernullable, 2021 Census
average_after_tax_incomeintegernullable, 2021 Census
household_countintegernullable, number of households in tract
latitudenumeric(10,7)centroid latitude
longitudenumeric(10,7)centroid longitude
created_attimestamptz

1004 census tracts for Montreal CMA, 986 with income data. Seeded from StatCan boundary shapefile + Table 98-10-0058-01 income CSV.

income_snapshots

Historical income data across all sources and geographies. Supports multi-year comparisons and trend analysis.

ColumnTypeNotes
iduuidPK
geo_typetextnot null. ct, fsa, cma, mrc
geo_codetextnot null. CT UID, FSA code, CMA code, etc.
yearintegernot null. Data year
sourcetextnot null. census, cra, isq
median_incomeintegernullable. Household median (census only)
median_income_after_taxintegernullable. Census only
average_incomeintegernullable. Household avg (census) or individual avg (CRA)
average_income_after_taxintegernullable. Census only
total_householdsintegernullable. Census only
total_filersintegernullable. CRA only
total_income_thousandsintegernullable. CRA total income in $1000s
created_attimestamptz

Unique index on (geo_type, geo_code, year, source).

Records: ~2,882 total — 986 CT×2021 (census), 951 CT×2016 (census), 1,945 FSA×7 years (CRA 2015-2021).

income_distributions

Income bracket distribution data from StatCan Table 98-10-0055. Shows number of households in each income bracket per geography/year.

ColumnTypeNotes
iduuidPK
geo_typetextnot null. cma
geo_codetextnot null. 462 for Montreal CMA
yearintegernot null. 2016, 2021
sourcetextnot null. census
bracketsjsonb[{label, min, max, count, pct}, ...] (19 brackets)
total_householdsintegernullable
created_attimestamptz

Unique index on (geo_type, geo_code, year, source). 2 records (Montreal CMA × 2 census years).

rent_snapshots

CMHC Rental Market Survey average rent data by zone/subdivision.

ColumnTypeNotes
iduuidPK
geo_typetextnot null. zone or csd
geo_codetextnot null. Zone/CSD identifier
geo_nametextnot null. Human-readable name
yearintegernot null. 1998–2025
sourcetextnot null. cmhc
avg_rent_studiointegernullable, in cents
avg_rent_1brintegernullable, in cents
avg_rent_2brintegernullable, in cents
avg_rent_3brintegernullable, in cents
avg_rent_totalintegernullable, in cents
vacancy_ratenumeric(4,1)nullable, percentage
universeintegernullable, total units surveyed
created_attimestamptz

Unique index on (geo_type, geo_code, year, source). Requires manual CMHC portal download.

points_of_interest

Points of interest (transit stops, schools) pre-seeded from open data sources for nearby queries on listing detail pages.

ColumnTypeNotes
iduuidPK
source_idtextunique, not null. Format: stm:{stop_id} or meq:{code}
sourcetextstm_gtfs or meq
categorytextmetro, bus, train, school
subcategorytextgreen_line, orange_line, blue_line, yellow_line, primary, secondary, cegep, university
nametextPOI name
name_frtextFrench name
latitudenumeric(10,7)
longitudenumeric(10,7)
metadatajsonbBus: { routes: ["67", "185"] }. Schools: { schoolBoard, language }
is_activebooleandefault: true. Set false on re-sync when POI no longer exists
synced_attimestamptzlast sync timestamp
created_attimestamptz

Indexes on (category) and (latitude, longitude) where is_active = true.

rent_benchmarks

Precomputed rent percentiles per neighborhood + bedroom count. Recomputed nightly at 3am from 12 months of active+archived listings. Used for AutoTrader-style price rating badges.

ColumnTypeNotes
iduuidPK
neighborhood_iduuidFK neighborhoods, not null
bedroomsintegernot null. 0=studio, 1-3, 4=4+
medianinteger50th percentile rent in cents
p20integer20th percentile
p40integer40th percentile
p60integer60th percentile
p80integer80th percentile
sample_countintegernot null, default 0
computed_attimestamptznot null

Unique index on (neighborhood_id, bedrooms).

listing_price_history

Tracks price changes detected by the scraper during re-scraping. Used for "Price Drop" badges and the priceDrops filter.

ColumnTypeNotes
iduuidPK
listing_iduuidFK listings, not null
old_priceintegernot null, in cents
new_priceintegernot null, in cents
changed_attimestamptznot null, default now

Index on (listing_id, changed_at).


Scraper Schema (scraper.*)

Source: services/scraper/src/db/schema.ts

All scraper tables live in the scraper PostgreSQL schema, isolated from the public schema.

scraper.scrape_runs

One row per scraper execution cycle.

ColumnTypeNotes
iduuidPK
platformplatformkijiji, kangalou, etc.
started_attimestamptz
completed_attimestamptznullable
statustextrunning, completed, failed
listings_foundintegertotal listings seen
listings_newintegerfirst-time listings
listings_updatedintegerre-scraped with changes
error_messagetextnullable
configjsonbrun configuration

scraper.raw_listings

Core scraped data with full pipeline status tracking.

ColumnTypeNotes
iduuidPK
platformplatformsource platform
platform_idtextID on source site
platform_urltextURL on source site
scrape_run_iduuidFK → scrape_runs
statusscrape_statusraw → normalized → imported
imported_listing_iduuidFK → public.listings
content_hashtextSHA hash of raw data; used to detect changes on re-scrape
raw_*text/jsonbraw scraped fields
norm_*text/int/jsonbnormalized fields
first_scraped_attimestamptzfirst seen
last_scraped_attimestamptzmost recent scrape

Unique index on (platform, platform_id).

scraper.raw_images

ColumnTypeNotes
iduuidPK
raw_listing_iduuidFK → raw_listings, cascade
source_urltextoriginal URL
local_pathtextdownloaded file path
download_statustextpending, downloaded, failed
file_sizeintegerbytes

scraper.pipeline_events

Per-message activity log. One row per pipeline stage processed. Auto-pruned after 7 days.

ColumnTypeNotes
iduuidPK
stagepipeline_stagescraped, normalized, imported, images, published, error
raw_listing_iduuidFK → raw_listings, nullable
platformplatformsource platform
duration_msintegerprocessing time
resultjsonbstage-specific details (see monitoring docs for schema per stage)
errortexterror message if failed
created_attimestamptzindexed for querying

scraper.stats_snapshots

Hourly aggregate throughput counters. Persisted from the in-memory StatsCollector. Auto-pruned after 90 days.

ColumnTypeNotes
iduuidPK
period_starttimestamptzstart of hourly period
period_endtimestamptzend of hourly period
countersjsonb{scraped, scrapedNew, normalized, imported, ...}
created_attimestamptz

scraper.scraper_state

Persistent key-value store for cursor state (e.g., kijiji:nextDeepPage).

ColumnTypeNotes
keytextPK
valuejsonb
updated_attimestamptz