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)
);
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_numberis 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. -
sourceis 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 });
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;
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,hreflangalternates 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' })vsen-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
}
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/Offerstructured 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)