1 OAK MLS

Database Schema

Supabase Postgres schema for the 1 OAK MLS Platform - all tables are multi-tenant with workspace_id

Database Schema (Supabase Postgres)

All tables are multi-tenant with workspace_id as the partition key.

Entity Relationship Overview

workspaces
    ├── workspace_domains (1:many)
    ├── mls_connections (1:many, typically 1 per provider)
    │       └── import_tasks (1:many)
    ├── field_mappings (1:many, versioned)
    ├── listings (1:many)
    │       ├── listing_media (1:many, MLS synced)
    │       └── listing_photos (1:many, agent uploads)
    ├── user_profiles (1:many)
    │       ├── favorites (1:many)
    │       └── saved_searches (1:many)
    ├── compliance_profiles (1:1)
    └── sync_runs (1:many)

Tables

1. workspaces

Root tenant entity. Every other table references this.

ColumnTypeNotes
iduuidPK, auto-generated
slugtextUnique, URL-safe identifier (e.g., lainey-levin)
nametextDisplay name
statustextactive | paused | archived
created_attimestamptz
updated_attimestamptz

2. workspace_domains

Custom domains for each workspace's client site.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
domaintexte.g., laineylevin.com
is_primarybooleanDefault false
created_attimestamptz

Unique: (workspace_id, domain)

3. mls_connections

Bridge/RESO API connection configuration per workspace.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
providertextbridge | future providers
dataset_idtextBridge dataset identifier (OData path segment)
token_ciphertexttextEncrypted — never log or return in API
token_typetextserver | oauth
base_urltextDefault: https://api.bridgedataoutput.com/api/v2/OData
statustextpending | active | error | paused
sync_cadencetextmanual | hourly | daily
last_synced_attimestamptz
last_sync_cursortextOptional checkpoint/cursor
created_attimestamptz
updated_attimestamptz

Unique: (workspace_id, provider)

4. field_mappings

Versioned RESO → canonical field mapping rules.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
providertextbridge
versionintIncrement on changes
is_activebooleanOnly one active per workspace/provider
mappingjsonbSee mapping format below
created_attimestamptz
updated_attimestamptz

Unique: (workspace_id, provider, version)

Mapping JSON format:

{
  "source_listing_key": "ListingKey",
  "mls_number": "ListingId",
  "standard_status": "StandardStatus",
  "modification_ts": "ModificationTimestamp",
  "property_type": "PropertyType",
  "list_price": "ListPrice",
  "bedrooms": "BedroomsTotal",
  "bathrooms": "BathroomsTotalInteger",
  "living_area_sqft": "LivingArea",
  "year_built": "YearBuilt",
  "address_full": "UnparsedAddress",
  "city": "City",
  "state": "StateOrProvince",
  "postal_code": "PostalCode",
  "subdivision": "SubdivisionName",
  "building_name": "BuildingName",
  "latitude": "Latitude",
  "longitude": "Longitude",
  "public_remarks": "PublicRemarks"
}

5. listings

Canonical listing data. Source of truth after normalization.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
Source identifiers
source_providertextbridge
source_dataset_idtextDataset this came from
source_listing_keytextListingKey from source
mls_numbertextPublic MLS number
Status & timestamps
standard_statustextActive | Pending | Closed | etc.
modification_tstimestamptzFrom source
list_datedate
close_datedate
Address
address_fulltextFull formatted address
street_numbertext
street_nametext
unit_numbertext
citytext
statetext
postal_codetext
Location taxonomy
subdivisiontextNeighborhood
building_nametextCondo/building
communitytextOptional
countytext
Geo
latitudedouble precision
longitudedouble precision
Property facts
property_typetextCondominium, Single Family, etc.
bedroomsint
bathroomsdouble precision
living_area_sqftint
lot_size_sqftint
year_builtint
Pricing
list_priceint
close_priceint
price_per_sqftintComputed or from source
Content
public_remarkstext
photo_countint
Flags
is_activebooleanFor soft-delete
import_task_iduuidFK → import_tasks (which task imported this)
Raw data
rawjsonbOriginal source payload
created_attimestamptz
updated_attimestamptz

Unique: (workspace_id, source_provider, source_listing_key)

Indexes:

  • (workspace_id, standard_status)
  • (workspace_id, city)
  • (workspace_id, subdivision)
  • (workspace_id, building_name)
  • (workspace_id, modification_ts DESC)

6. listing_media

Photos and media from MLS sync.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
listing_iduuidFK → listings
urltextPhoto URL (hotlinked from MLS)
sort_orderintDisplay order
captiontextOptional
created_attimestamptz

Unique: (listing_id, url)

6a. listing_photos

Agent-uploaded photos (takes priority over listing_media).

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
listing_iduuidFK → listings
storage_pathtextSupabase Storage path
urltextPublic URL
thumbnail_urltextResized version (optional)
sourcetextupload or import
sort_orderintDisplay order
is_primarybooleanPrimary photo for listing
captiontextOptional caption
alt_texttextAccessibility text
mime_typetextFile MIME type
size_bytesintFile size
widthintImage width (optional)
heightintImage height (optional)
uploaded_byuuidFK → auth.users
created_attimestamptz

Indexes:

  • (listing_id) — Photo lookup
  • (listing_id, is_primary) — Primary photo lookup

6b. import_tasks

Configurable listing import tasks with categories.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
mls_connection_iduuidFK → mls_connections
nametextTask display name
descriptiontextOptional description
categorytextpersonal, office, search, custom
filter_configjsonbMLS query configuration
status_filterstext[]e.g., ['Active', 'Pending']
price_minintMinimum price filter
price_maxintMaximum price filter
max_import_limitintMax listings to import
field_mapping_iduuidFK → field_mappings (optional)
sync_cadencetextmanual, hourly, daily
is_enabledbooleanTask is active
is_agent_visiblebooleanShow in agent dashboard
last_synced_attimestamptzLast sync timestamp
last_sync_cursortextIncremental sync cursor
created_attimestamptz
updated_attimestamptz

Filter Config Examples:

Personal listings:

{ "agentMlsId": "12345" }

Office listings:

{ "officeMlsId": "BROKER123" }

Service area:

{ "cities": ["miami"], "subdivisions": ["coconut-grove"] }

7. user_profiles

Extends Supabase Auth users with workspace context.

ColumnTypeNotes
iduuidPK, FK → auth.users
workspace_iduuidFK → workspaces
roletextadmin | agent | client
full_nametext
created_attimestamptz
updated_attimestamptz

8. favorites

User's favorited listings.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
user_iduuidFK → auth.users
listing_iduuidFK → listings
created_attimestamptz

Unique: (workspace_id, user_id, listing_id)

9. saved_searches

Stored search queries for alerts.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
user_iduuidFK → auth.users
nametextUser-defined name
queryjsonbTypesense-compatible query params
created_attimestamptz
updated_attimestamptz

10. compliance_profiles

MLS attribution and disclaimer configuration.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces (1:1)
attribution_htmltextRequired MLS attribution text
disclaimer_htmltextLegal disclaimer
display_on_searchbooleanShow on search results
display_on_detailbooleanShow on listing detail
created_attimestamptz
updated_attimestamptz

Unique: (workspace_id)

11. sync_runs

Audit log of sync operations.

ColumnTypeNotes
iduuidPK
workspace_iduuidFK → workspaces
mls_connection_iduuidFK → mls_connections
import_task_iduuidFK → import_tasks (optional)
started_attimestamptz
finished_attimestamptz
statustextrunning | success | error
statsjsonb{ fetched, upserted, indexed, errors, duration_ms }
errortextError message if failed

Index: (workspace_id, started_at DESC)

Stats JSON:

{
  "fetched": 150,
  "upserted": 148,
  "indexed": 148,
  "errors": 2,
  "duration_ms": 45000
}

Row Level Security (RLS)

Enable RLS on user-facing tables:

ALTER TABLE workspaces ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE favorites ENABLE ROW LEVEL SECURITY;
ALTER TABLE saved_searches ENABLE ROW LEVEL SECURITY;

V1 Strategy:

  • Admin operations use service_role key (server-side only)
  • Client site uses server routes to fetch listings (no direct DB access from client)
  • User favorites/saved searches: RLS policies based on auth.uid()

Migration Order

  1. Extensions (pgcrypto)
  2. workspaces
  3. workspace_domains
  4. mls_connections
  5. field_mappings
  6. listings
  7. listing_media
  8. user_profiles
  9. favorites
  10. saved_searches
  11. compliance_profiles
  12. sync_runs
  13. Indexes
  14. RLS policies

On this page