DEV Community

Cover image for Architecting a Property Listing Platform for Dubai: Data Model, Feeds, Search and RTL
AI Dev Planner
AI Dev Planner

Posted on

Architecting a Property Listing Platform for Dubai: Data Model, Feeds, Search and RTL

Most "real estate website" projects start with a theme and a page builder. That works until the brokerage has 400 live listings, three listing sources, two languages and agents asking why the price on the website doesn't match the CRM.

At that point you're not building a website anymore. You're building a small data platform with a public front end. This post walks through how we'd architect a property listing platform for a Dubai brokerage — the decisions that matter, and the ones that bite you later if you skip them.

1. Start with the listing data model, not the homepage

The listing record is the heart of the system. In Dubai, a usable schema needs more than price and bedrooms:

CREATE TABLE listings (
  id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  reference        text UNIQUE NOT NULL,          -- agency ref, used by feeds
  purpose          text NOT NULL CHECK (purpose IN ('sale','rent')),
  completion       text NOT NULL CHECK (completion IN ('ready','off_plan')),
  property_type    text NOT NULL,                 -- apartment, villa, townhouse, office...
  community_id     int  REFERENCES communities(id),
  building_id      int  REFERENCES buildings(id),
  bedrooms         smallint,                      -- 0 = studio
  bathrooms        smallint,
  size_sqft        numeric(10,2),
  price_aed        bigint NOT NULL,
  service_charge   numeric(10,2),
  permit_number    text,                          -- marketing permit, shown where required
  location         geography(Point, 4326),
  status           text NOT NULL DEFAULT 'draft', -- draft, live, under_offer, sold, archived
  agent_id         uuid REFERENCES agents(id),
  source           text NOT NULL,                 -- crm, xml_feed, manual
  updated_at       timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE listing_translations (
  listing_id  uuid REFERENCES listings(id) ON DELETE CASCADE,
  locale      text CHECK (locale IN ('en','ar')),
  title       text NOT NULL,
  description text,
  slug        text NOT NULL,
  PRIMARY KEY (listing_id, locale)
);
Enter fullscreen mode Exit fullscreen mode

A few deliberate choices:

  • Translations live in their own table. Arabic isn't a column suffix (title_ar); it's a full locale with its own slug and SEO metadata.
  • Communities and buildings are entities, not free text. "Dubai Marina", "Marina", and "DUBAI MARINA " should never be three filters.
  • permit_number is a field from day one. Property advertising in Dubai runs through the Dubai Land Department's permit system, so the data model should carry it even if display rules are decided later with the client.
  • source is explicit, so you always know which system is allowed to overwrite a record.

2. Decide the source of truth (and write it down)

Most brokerages have listings in at least two places: a CRM and the portals (Property Finder, Bayut). If the website becomes a third editable copy, you've created a sync problem with no winner.

Pick one:

Pattern When it fits Trade-off
CRM is master → website imports Agency already lives in its CRM Website is read-only for listings
Website/platform is master → exports feeds You're replacing spreadsheets/old CRM You own feed generation & portal compliance
Hybrid by field Rare, complex orgs Needs field-level ownership rules

For imports, most real estate CRMs and portals speak XML feeds. Treat the feed importer as a proper job: fetch, validate, diff against the current state, upsert changed rows, and archive listings that disappeared — never hard-delete, because live URLs may be indexed.

for (const item of feedItems) {
  const parsed = ListingSchema.safeParse(normalize(item));
  if (!parsed.success) { report.invalid.push({ ref: item.reference, errors: parsed.error.issues }); continue; }
  const existing = await repo.findByReference(parsed.data.reference);
  if (!existing) await repo.insert(parsed.data);
  else if (hash(existing) !== hash(parsed.data)) await repo.update(existing.id, parsed.data);
  seen.add(parsed.data.reference);
}
await repo.archiveMissing({ source: 'xml_feed', keep: seen });
Enter fullscreen mode Exit fullscreen mode

Send the report to someone. Silent feed failures are how "the website is showing a sold villa" happens.

3. Search: Postgres goes further than you think

For a brokerage with hundreds to low thousands of listings, you rarely need Elasticsearch on day one. Postgres with PostGIS handles:

  • Faceted filters (purpose, completion, type, beds, price band) with composite indexes
  • Map search with a bounding-box query on location
  • Text search on titles/descriptions via tsvector (with a separate Arabic configuration or trigram index for Arabic text)
SELECT id, price_aed, bedrooms
FROM listings
WHERE status = 'live'
  AND purpose = $1
  AND price_aed BETWEEN $2 AND $3
  AND location && ST_MakeEnvelope($4, $5, $6, $7, 4326)
ORDER BY updated_at DESC
LIMIT 24;
Enter fullscreen mode Exit fullscreen mode

Add a dedicated search engine when you need typo tolerance across languages, or volumes that make facet counts slow.

4. Arabic and RTL: build it in, don't bolt it on

The failure mode is a "translate" plugin and a CSS file full of float: right !important. What actually works:

  • Route-level locales: /en/properties/... and /ar/properties/..., each with its own slug, hreflang alternates and canonical.
  • Set dir="rtl" on <html> for Arabic and use CSS logical properties (margin-inline-start, padding-inline-end, inset-inline-start) so one stylesheet serves both directions.
  • Mirror icons that imply direction (arrows, carousels), but not logos or phone numbers.
  • Format prices with Intl.NumberFormat('ar-AE', { style: 'currency', currency: 'AED' }) vs en-AE — and decide with the client whether to show Western or Arabic-Indic digits.
  • Test with real Arabic copy; layout breaks show up with real word lengths, not lorem ipsum.

5. Leads are the product — treat them like events

Every enquiry (form, WhatsApp click, call tap, portal lead) should become an event with context:

{
  "type": "lead.created",
  "channel": "whatsapp",
  "listing_ref": "MAR-2BR-1182",
  "locale": "ar",
  "utm": { "source": "google", "campaign": "marina-offplan" },
  "consent": true
}
Enter fullscreen mode Exit fullscreen mode

From there: dedupe by phone/email, route by rules (language, community, budget), push to the CRM via API/webhook, notify the agent, and escalate if there's no response within an agreed window. Store consent and keep personal data handling aligned with UAE data protection law (Federal Decree-Law No. 45 of 2021) — and agree hosting region with the client early.

6. Performance and SEO basics that matter more here

  • Every live listing gets an indexable URL and RealEstateListing/Offer structured data.
  • Images: AVIF/WebP, responsive srcset, lazy-load below the fold. Listing pages are image-heavy and Google's own (2016) research found 53% of mobile visitors leave pages that take over 3 seconds.
  • Archived listings return a helpful page (similar properties) instead of a 404 storm.

Wrapping up

The difference between a real estate website and a real estate platform is mostly invisible: data ownership, sync rules, lead handling and bilingual architecture. Get those right and the front end is the easy part.

If you're scoping something similar for a GCC client, our notes on web development for GCC businesses cover hosting regions and working hours, and the Real Estate & PropTech page lists the integrations we typically plan for.

What's your go-to stack for listing search — Postgres all the way, or straight to a search engine? Curious how others handle feed sync.

Visual: conceptual illustration, not a client project.

Top comments (0)