Skip to content

Database

45 tables defined in ordered SQL migrations under packages/database/migrations/. PostgreSQL with PostGIS for geographic data; @repo/database is a Kysely client (on the pg driver) with generated types — there is no ORM.

GroupTables
Users & AuthUser, Session, Account, Verification, UserAddress
Agents & TeamsAgentProfile, AgentServiceArea, Team, TeamMember, TeamInvitation
PropertiesProperty, PropertyFeature, PropertyImage, PropertyVideo, FloorPlan, PropertyUnit, UnitInventory, PropertyAgent, PropertyList, PropertyListItem
ListingsListingVerification, Disclosure, RentalTerms
Leads & CommsInquiry, Tour, Note, Task, Notification
TransactionsOffer, Transaction, Commission, Document, Payment
EngagementFavorite, SavedSearch, PropertyView, Review
Market DataComparable, PriceHistory, MarketReport
ConfigRedisConfig

Read packages/database/src/enums.ts (const-object values) and packages/database/src/generated/db.ts (column types) before using any enum value — guessing is the most common bug class in this repo.

EnumValues
UserRoleINDIVIDUAL, AGENCY_ADMIN, ADMIN
PropertyTypeHOUSE, APARTMENT, SUITE_ROOM, LAND
PropertyStatusDRAFT, ACTIVE, PAUSED, SOLD, RENTED, ARCHIVED
ListingTypeSALE, RENT
TeamTypeAGENCY, GROUP, CONSTRUCTION_COMPANY
TeamRoleLEAD, ADMIN, MEMBER
Tour.statusREQUESTED (not PENDING), CONFIRMED, RESCHEDULED, COMPLETED, CANCELLED, DENIED
InquiryStatusNEW, CONTACTED, IN_PROGRESS, CLOSED, CONVERTED

The Tour.status = REQUESTED vs PENDING mismatch has caused multiple bugs — call it out explicitly in PR review.

erDiagram
    User ||--o| AgentProfile : "optional"
    AgentProfile ||--o{ TeamMember : "joins"
    TeamMember }o--|| Team : "belongs to"
    User ||--o{ Property : "owns"
    Property ||--o{ PropertyAgent : "assigned to"
    AgentProfile ||--o{ PropertyAgent : "manages"
    Property ||--o{ PropertyImage : "has"
    Property ||--o{ PropertyUnit : "has units"
    PropertyUnit ||--o{ UnitInventory : "has inventory"
    Property ||--o{ Inquiry : "receives"
    Property ||--o{ RentalTerms : "has terms"
    User ||--o{ Favorite : "saves"
    User ||--o{ SavedSearch : "creates"
    Property ||--o{ Transaction : "results in"
    PropertyList ||--o{ PropertyListItem : "contains"
    AgentProfile ||--o{ PropertyList : "owns"
TypeRequired fieldsOptional fields
HOUSEbedrooms, bathrooms, constructedArea, pricelotSize, yearBuilt, parkingSpaces
APARTMENTbedrooms, bathrooms, constructedArea, priceyearBuilt, parkingSpaces
SUITE_ROOMbedrooms (≥1), bathrooms, constructedArea, priceyearBuilt, parkingSpaces
LANDlotSize, priceyearBuilt, parkingSpaces

Validation rules live in @repo/validation (Zod), shared by web form actions and tRPC inputs.

DRAFT → ACTIVE → PAUSED → ACTIVE (resume)
→ SOLD / RENTED (terminal)
DRAFT → ARCHIVED (soft delete)

The price_history trigger fires only when the property is in ACTIVE status — INITIAL on publish/reactivate, PRICE_REDUCTION / PRICE_INCREASE on edits. Price-drop badges on property cards are computed from this table during worker re-indexing (30-day window).

85+ RLS policies across 28 database tables. User data is completely isolated at the DB level:

  • Public users see only ACTIVE properties; owners see all statuses.
  • Transaction records are visible only to participants.
  • Agent profile data is public; user PII is private.

Policies are declared in the 0003_triggers_and_functions.ts migration and applied by db:migrate (and re-applied by the db:reset script) after the baseline schema is created.

Most tables have IDs defaulted to generate_*_id(). The generator functions are created by the 0001_extensions_and_id_functions.ts migration and must exist before the 0002_baseline_schema.ts table-create migration runs, which is why the migration chain is strictly ordered (0001 functions → 0002 schema → 0003 triggers) and the db:reset script replays it instead of dropping and recreating in one atomic step. See Conventions → Monorepo.

Terminal window
cd packages/database && bun run db:reset # drops + recreates schema with PostGIS,
# replays the migration chain
# (0001 functions → 0002 schema → 0003 triggers),
# verifies state
redis-cli -u "$REDIS_URL" FLUSHDB

packages/database/scripts/db-reset.ts is the source of truth.