Articulate · RE stack · Operator build plan v2

dubai.co.uk: property portal build plan

A portal, not a brochure site. About 1,000 public SSR pages carry the full numbers. Each page has hooks into signed-in tools (map, DLD browser, saved comps, alerts, deal tracker) for buyers, investors and agents. Every value has a source tag. The plan runs as 3 weeks of shippable tickets.

Updated Mon 5 Oct 2026, 12:55 GST Sprint window 5–25 Oct 2026 Data SoT DLD + PF matches (ours) Plan host dubai-co-uk-plan.articulate-ai.work (cwdeploy · never here.now)

§0Rules (non-negotiable)

R1 Sales

  • Every public page has ≥1 portal hook (save, alert, compare, book viewing).
  • Every hook writes an events row with entity_type and entity_id, and opens a lead path.

R2 Never hide numbers

  • All KPIs are in SSR HTML tables.
  • Charts are a layer on top of a table, never the only copy.
  • No bot/human cloaking.
  • Every table has a CSV twin under /data/.

R3 Data provenance

  • DLD wins for price and date. PF wins for unit and floor.
  • Never average across sources. Area medians are DLD-only.
  • Every value carries a source tag: ourspubliclicensed, plus [D]/[B]/[A] (DLD data / benchmark / assumed).
DXBinteract = UX inspiration only. We borrow the area×category toggles, scorecard tiles, handover and supply charts. We never scrape, mirror or quote their figures. The DXBinteract MCP may be used internally as a QA cross-check. Its output must never be written into any public table.

§1Portal map: public SEO layer → portal tools → signed-in areas

PUBLIC (SSR, indexable, ~1,000 URLs)        HOOKS on every page             PORTAL TOOLS (public read, save = sign-in)     SIGNED-IN AREAS (RLS)
dubai.co.uk/developers/{slug}          ─┐   [Save project] [Alert me]  ──►  /map?area=&type=&beds=  price-change map     /app            buyer/investor home
dubai.co.uk/master-projects/{slug}      │   [Save comps] [Compare]          /dld?project=2621       DLD browser 1.66M tx  /app/saved      comps · projects · searches
dubai.co.uk/areas/{slug}                ├─► [Open in map / DLD]        ──►  /compare?p=2621,3637    side by side          /app/alerts     DLD-delta alerts
dubai.co.uk/projects/{dld-no}-{slug}    │   [Book viewing] [WhatsApp]       /calc/cost|mortgage|yield|golden-visa         /app/deals      my deal tracker + docs
dubai.co.uk/buildings/{slug}            │   [Brochure]                                                                    /investor       portfolio + valuations
dubai.co.uk/reports/{yyyy}/{area}      ─┘                                                                                 /agent          pipeline · leads · viewings
dubai.co.uk/guides/* · /data/* · /method                                                                                   /ops            SLA · attribution · ingest
RoleGetsSignup triggerSales job
anonAll public pages, read-only map and DLD browser, CSV downloadsnoneSEO/AIO traffic → hook click
buyerSaved projects and comps, alerts, calculators with saved scenarios, deal tracker, document vaultSave, alert, book viewing, brochure downloadLead → viewing → offer
investorEverything a buyer gets, plus a portfolio of owned units: comps-based value, yield and handover trackerAdd owned unit; yield calc saveResale and rental mandates, repeat buys
agentPipeline kanban, assigned leads, viewings, deal docs, commission ledger, saved comps shared to clientsInvite only (BRN verified)Close deals; SLA compliance
ops / adminSLA board, attribution, ingest status, content QA, asset rightsInternalThroughput and data trust

§2URL inventory (~1,000 public pages, exact patterns)

TemplateURL patternExampleCountSlug key (stable)Inclusion rule
T-DEV developer/developers/{dev-slug}/developers/r-evolution80DLD developer no. → slug map≥1 DLD project with ≥5 unit sales since 2020
T-MP master project/master-projects/{mp-slug}/master-projects/business-bay40DLD master_project≥3 projects or ≥200 sales since 2020
T-AREA area hub/areas/{area-slug}/areas/business-bay110DLD area (area_id)≥100 sales since 2020
T-PRJ project/projects/{dld-no}-{slug}/projects/2621-eywa-dubai420DLD project number (never changes)Top 420 by sales count L36M, plus all off-plan with an escrow account
T-BLD building/buildings/{bld-slug}/buildings/volante200DLD building + PF building idReady buildings with ≥20 sales, or ≥10 PF-matched units
T-RPT yearly report/reports/{yyyy}/{area-slug}/reports/2025/business-bay60year + areaTop 30 areas × 2024, 2025
T-CMP compare/compare/{a}-vs-{b}/compare/business-bay-vs-downtown-dubai40pair of area slugsTop pairs by co-search (GSC) / adjacency
T-GDE guides (overlay hubs)/guides/{topic}/guides/golden-visa-property, /guides/buying-process, /guides/mortgages, /guides/fees, /guides/off-plan, /guides/uk-buyers, /guides/schools20topicHand-written, with tables from ref_* rows
T-TOOL tool landings/map /dld /compare /calc/*/calc/mortgage10—SSR intro + the default table, then the app mounts
T-DATA open data docs/data, /data/{dataset}/data/dld-transactions10datasetDataset JSON-LD + column dictionary
Trust/method /faq /about /agents/{brn}/method10——
Total1,000The generator writes the exact inventory into page_inventory and sitemap-{template}.xml.

Not indexed: /app/*, /investor/*, /agent/*, /ops/*, plus filtered tool states (/map?…, /dld?…). Filtered states canonicalise to the tool landing.

§3T-DEV developer page: exact fields

BlockFieldSourceTable.columnRender rule
HeaderDisplay name, legal namepublic DLD developer registrydevelopers.name, .legal_nameH1 = display name. Legal name shown under it.
DLD developer no.public DLDdevelopers.dld_dev_noPlain text plus a link to the DLD lookup
Projects by status (count)public DLD project statuscount(dld_projects) by status4 tiles: Active · Completed · Pending · Cancelled
Units delivered / under constructionpublic DLDsum(dld_projects.units) by statusTiles
Escrow accountspublic DLDdld_projects.escrow_noListed per project row
Track record oursOn-time handover %Derived from DLD planned vs actual completiondev_scores.on_time_pctShow n projects. Hide the tile if n<3 and say why in text.
Median delay (months)Deriveddev_scores.median_delay_mShow n
Overdue active projectsDerived: planned completion < today and % < 100dev_scores.overdue_nRed count with a list of the projects
Resale premium (median resale psf ÷ launch psf − 1)Derived from DLD sales (first off-plan sale vs later resales)dev_scores.resale_premium_pctOnly where both legs have n≥5
Market oursSales L12M: count, AED volumeDLD transactionsmv_dev_sales_l12mNumber tiles plus a table
Median AED/sqft L12M vs prev 12MDLDmv_dev_sales_l12m.median_psfYoY % only if both windows have n≥5
Projects tableDLD + oursdld_projects ⋈ mv_project_kpisColumns: project · area · status · % complete · planned handover · units · sales · median psf · last sale date. Sortable, SSR.
MediaLogo, corporate videolicensed developer kitassets (entity_type=developer)Logo only if the licence allows. Otherwise text only.
Hooks“Alert me on new launches by {dev}”, “Compare developers”—saved_searches (filter dev_no)Anon clicks open the sign-in modal and return to the page.
Open data/data/developers/{dev-slug}.csv—projects tableSame rows as the HTML

§4T-MP master project and T-AREA area hub: exact fields

T-MP /master-projects/{mp-slug}
FieldSource → tableRender
Name, master developerDLD master_project → master_projectsH1, with a link to T-DEV
DLD areas coveredmp_areas (m:n)Chips linking to T-AREA
Projects (count, list)dld_projects where mp_idTable, same columns as T-DEV
Total units, units by handover yeardld_projects.units, planned_completionSupply table plus a bar chart (M-SUP)
Sales L12M, median AED/sqft by type × bedsmv_mp_kpisMatrix table (Flat/Villa/Townhouse × Studio…5BR)
Median rent by beds, gross yieldEjari → dld_rents (M-YLD)Table, with n per cell
Boundary polygonOSM / our digitised GeoJSON → geo_shapesStatic SVG in SSR. MapLibre layers on top.
OverlaysM-SCH, M-LOC, M-TRF, M-SHP summaryCompact cards
T-AREA /areas/{area-slug} (DLD area, DLD-only medians)
FieldSource → tableRender
Name, DLD area id, aliasesareasH1 plus an “also filed as” note (e.g. western Business Bay towers filed under DLD area Burj Khalifa)
Monthly median AED/sqft, 36 monthsmv_area_monthlyTable (36 rows) plus a line chart
Volume (count, AED) by month; off-plan sharemv_area_monthlySame table
Top 20 projects / buildings by sales L12Mmv_project_kpisRanked table with links
Rent medians, yielddld_rentsM-YLD
Share of sales ≥ AED 2Mmv_area_kpis.gv_shareM-GV badge
OverlaysAll of §7Full modules
HooksAlert on new sales in the area; open /map?area=—

§5T-PRJ project page /projects/{dld-no}-{slug}: exact fields

BlockFieldSourceTable.columnRender rule
Identity + statusDLD project no., name, aliasespublic DLDdld_projects.project_no, name; aliasesH1 = marketing name. “DLD: #2621 EYWA DUBAI” line.
Developer, master project, areaDLDdev_id, mp_id, area_idBreadcrumb plus links
Status, % complete, inspection dateDLD project statusstatus, pct_complete, inspected_onProgress bar and the number in text
Planned completion; overdue flagDLDplanned_completionRed “Overdue since {date}” if past and <100%
Escrow account no.DLDescrow_noPlain text
Plot sqm, floors, units, bed mixDLD (+ brochure for comparison)plot_sqm, floors, units, mix_jsonTable. Conflicts go to the M-CONF box.
Lat/lonDLD inspection pin / OSMlat, lon, geo_sourceFeeds all overlays
Service charge AED/sqft/yrpublic DLD Service Charge Indexservice_chargesValue plus year. “Not yet published” for off-plan.
Golden Visa eligible shareDerivedmv_project_kpis.gv_shareM-GV badge
Market oursSales n (segment), first/last saleDLD tx (lease-to-own and outliers excluded)mv_project_kpisTiles
Median AED/sqft (all · by beds · off-plan vs ready)DLDmv_project_kpisTable with n per cell. Cells with n<3 show “n=1: 4,915”, not a median.
Median price AED, median size sqftDLDsameTiles
Quarterly median psf + nDLDmv_project_quarterlyTable plus chart
All transactionsDLD + PF match (unit/floor)dld_transactions ⋈ pf_matchesColumns: date · procedure · beds · sqft · AED · AED/sqft · unit · floor · match conf. Excluded/flagged rows go in a collapsed “Excluded (n)” table with the reason.
Comps: 500 m radius, same segment, L24MDLDmv_compsTable of top 10 projects by sales. “Save these comps” hook.
Rent + yieldEjari + [B] benchmarkdld_rentsM-YLD with label
ConflictsUnit counts / storeys by sourceourssource_conflictsM-CONF box: every source with its value. Never silently resolved.
MediaBrochure, floorplans, payment plan, video, gallerylicensed/oursassets§8. Key numbers extracted into HTML.
OverlaysM-LOC, M-TRF, M-SCH, M-SHP, M-FIN, M-GV, M-TXN, M-SUP, M-UK§7Fixed order. Each one is an <section id> anchor.
HooksSave project · Alert on new sale · Save comps · Open /dld?project={no} · Open /map?project={no} · Book viewing · WhatsApp · Brochure (sign-in if licence-gated)

§6T-BLD building /buildings/{slug} and the unit view (portal)

Field (T-BLD, public)Source → table
Name, project link, DLD building name, PF building URLbuildings
Floors, units, completion year, height m (OSM)buildings, osm_buildings
Floor-band median psf (L1–5, 6–15, 16–30, 31+) with nmv_building_floorband (PF floor × DLD price)
Unit table: unit · floor · beds · sqft · last AED · last date · n salespf_matches ⋈ dld_transactions. Only matched pairs.
Repeat-sale pairs (buy → sell, CAGR)mv_unit_history (history_n ≥ 2)
Rent by beds; yield; service chargeM-YLD, service_charges
Overlays as T-PRJ (inherits the project lat/lon)§7
Unit view /app/units/{unit_key} (signed-in, noindex)Why gated
Full matched history for one unit, plus a comps-based value band (P25–P75 psf × size)Unit-level focus is a lead signal (owner or buyer). Saving requires an account.
“I own this”: adds the unit to the investor portfolio, with valuation alertsInvestor acquisition (resale and rental mandates)
“Ask agent about this unit”: lead with unit_keyHighest-intent lead
Public pages still show each unit's sale numbers in the building table (R2). The gate covers only tools and saving, never the data.

§7Overlay modules: data source · API/table · render rules · attach

A module is a server component <Overlay id="M-XXX" entity={…}/>. It reads (entity_type, entity_id, lat, lon) and renders an SSR table and a source line (“Source: KHDA 2025-26 · checked 2026-10-05”). If it has no data, it prints “No data: {reason}”. It never disappears silently.

IDModuleData source (type)Ingest / API → tableRender rule (SSR)Attaches toSchemaWk
M-LOCLocationpublic OSM (Geofabrik GCC extract), DLD nearest_metro / nearest_mall / nearest_landmark, our geocodeosm2pgsql → pois, osm_buildings. PostGIS ST_Distance → entity_distancesLat/lon, nearest metro (m, walk min at 80 m/min), straight-line distance to Burj Khalifa, DIFC, DXB, Dubai Marina. Static SVG map (Protomaps PMTiles, self-hosted). No Google tiles.MP, AREA, PRJ, BLDPlace + GeoCoordinates2
M-TRFTraffic / commutepublic OSRM on OSM (free-flow); RTA Salik gate list. licensed TomTom Matrix or Google Routes for live peak times (signed-in only, not cached)OSRM container → drive_times(entity, anchor, mode, min_freeflow, km, salik_gates) for 8 anchors: DIFC, Downtown, DXB, DWC, Marina, Internet City, Dubai Hills, the entity's top-rated schoolPublic table: anchor · km · free-flow min · Salik gates · Salik AED per trip, labelled “free-flow (OSRM)”. A “Peak now” column loads live for signed-in users only, since licence terms forbid publishing or caching it.AREA, PRJ, BLDtable2
M-SCHSchoolspublic KHDA school directory, inspection ratings, fee rangesIngest → schools(khda_id, name, curriculum, rating, rating_year, fee_min_aed, fee_max_aed, lat, lon); geocode → entity_schools (≤5 km, rank)Top 8 within 5 km: name · curriculum · KHDA rating (year) · fees AED · km · drive min (M-TRF). Count of Outstanding/Very good schools within 3 km as a tile.AREA, PRJ, BLD, /guides/schoolsItemList of School2
M-SHPShopping / daily lifepublic OSM POIs, DHA facility list (hospitals/clinics)pois where category in (mall, supermarket, pharmacy, hospital, clinic, gym, park, beach) → entity_poi_counts(1 km, 3 km)Counts table (1 km / 3 km) plus nearest named mall, supermarket and hospital with km. No ratings, since review data is licensed.AREA, PRJ, BLDtable2
M-FINFinancepublic CBUAE LTV caps, EIBOR; DLD fee table. [A] bank margin until a broker rate feed (licensed) is signedref_ltv_rules(buyer_type, value_band, property_state, max_ltv, source_url, checked_at), ref_rates, ref_feesWorked example at the page's median price, in HTML: deposit, loan, monthly payment (25y), cash-to-close. The interactive /calc/mortgage?price= enhances it. Assumptions are labelled [A].PRJ, BLD, AREA, /guides/mortgagesFAQPage (guide)2
M-GVGolden Visapublic ICP / DLD property-investor visa rules (AED 2M threshold; re-verify at build)ref_golden_visa_rules(rule, value, source_url, checked_at); derived gv_share = share of DLD unit sales ≥ AED 2MBadge: “15 of 15 recorded unit sales here ≥ AED 2M.” 4-line eligibility checklist (value, title deed / Oqood, mortgage conditions, joint ownership) with a link to /guides/golden-visa-property.PRJ, BLD, AREAHowTo + FAQPage on guide2
M-TXNTransaction processpublic DLD procedures and fees (Forms A/B/F/I, NOC, trustee centre, Oqood)ref_process_steps(path, step_no, step, who, docs, fee_rule, typical_days) with paths ready_cash, ready_mortgage, offplan_primary, offplan_resalePath auto-selected from project status (EYWA = off-plan resale). Step table with AED fees computed at the median price. These steps are the deal stages in §10, so the CTA is “Start this process with an agent”.PRJ, BLD, /guides/buying-processHowTo2
M-YLDRent & yieldpublic DLD open-data rents (Ejari) export; [B] nearby comps when the building has nonedld_rents(contract_id, start, area_id, project, beds, sqm, annual_aed) → mv_rent_kpisMedian annual rent by beds (n). Gross yield = rent ÷ median price. Benchmark rows say which buildings they come from.AREA, MP, PRJ, BLDtable2
M-SUPSupply & handoverspublic DLD project statusdld_projects → mv_supply(area_id, year, units)Units handing over by year in the area / within 1 km. Overdue unit count.AREA, MP, PRJtable2
M-CONFSource conflictsourssource_conflicts(entity, field, source, value, url)Yellow box listing each source's value. The value used in KPIs is marked.PRJ, BLD—1
M-UKUK buyerpublic CBUAE / ECB daily FX; HMRC linksref_fx(date, aed_gbp)GBP next to every AED headline figure, with the FX date. UK-time video-viewing slots. “UK tax on overseas rental income may apply: HMRC link. Not advice.”All public—2
M-TRUSTCompliancepublic DLD broker register (BRN/ORN), Trakheesi ad permitsagents(brn, orn, …), ad_permits(entity, trakheesi_no, expires)Any page with a priced offer or brief shows the agent BRN, company ORN and Trakheesi no. If the permit is missing, the price CTA is hidden.PRJ, BLD, briefsRealEstateAgent3
M-DEALDeal CRM stateoursdeals, leads (RLS)Anon sees the CTA block. A signed-in user with an open deal on this entity sees a stage chip: “O1 Offer made · 2 d · agent name (BRN)”.PRJ, BLD, unit viewnever in anon HTML3

§8Marketing assets: where PDFs, videos and galleries live, and how they link

Asset typeStored atPublic URLLinked fromText extracted into HTMLGateSchema
Brochure PDFR2 bucket dcu-assets → /{entity_type}/{entity_id}/brochure/{yyyy-mm}-{slug}.pdfassets.dubai.co.uk/project/2621/brochure/2024-02-eywa-offer.pdfT-PRJ #media, T-DEV media rowUnit count claims, sizes, payment plan %, starting price, amenities. Shown as a “Brochure says” table beside the DLD values.Public if the licence allows, else sign-in (numbers still on page)DigitalDocument
Floorplans/{…}/floorplans/{unit-type}.pdf|webpsame CDNT-PRJ and T-BLD unit-type rowsUnit type, beds, internal/total sqftPublicImageObject / FloorPlan
Payment plan/{…}/payment-plan.pdf + rows in payment_plans—T-PRJ M-FINMilestone % table (booking, construction stages, handover, post-handover)Publictable
Video (own)Our YouTube channel (master file kept in R2 /video/)YouTube embed (lite-youtube, click to load)T-PRJ hero, T-AREATranscript (VTT → HTML <details>)PublicVideoObject + transcript
Video (developer)Not rehostedEmbed from the developer's official YouTubeT-PRJ #mediaTitle, durationPublicVideoObject (embedUrl)
Gallery / renders / 3DR2 /{…}/gallery/{n}-{w}.webp (480 / 960 / 1920)CDNT-PRJ gallery strip; 3D viewer (e.g. eywa.articulate-ai.work GLB) as #3dAlt text, caption, credit, licencePublicImageObject (creditText, license)

Table assets: asset_id, entity_type, entity_id, type, title, r2_key, cdn_url, mime, bytes, sha256, pages, duration_s, width, height, source(developer_portal|own_shoot|agent|youtube), licence_ref, rights_expiry, gated, published, extracted_json, created_at. A nightly job unpublishes any asset past rights_expiry. Ingest: assets-ingest {entity} {file} uploads to R2, hashes the file, runs pdftotext, an LLM extracts fields into extracted_json, and a human signs off before publish.

§9Portal tools and signed-in areas (routes)

RouteWhoWhat it doesReadsWritesBuilt from
/mapanon read · user saveArea × type × beds price-change map: median psf, volume, top buildings, monthly changemv_area_monthly, mv_project_kpissaved_searchesdubai-prices prototype
/dldanon read · user save/exportExplorer over 1,661,875 DLD transactions plus the PF unit/floor match (2,964 tx / 2,332 units in batch 1)dld_transactions, pf_matchessaved_compsArticulate/DLD-Browser/dld-data
/compareanon2–4 projects or areas side by side (KPIs and overlays)mv_*—new
/calc/{cost|mortgage|yield|golden-visa}anon · user saveCalculators backed by the ref_* tablesref_*scenariosnew
/appbuyer, investorHome: watchlist changes since last visit, new sales in saved areas, open deal stageRLS own rows—new
/app/savedbuyer, investorSaved projects, comps sets (tx id lists), searches. Share link to an agent.watchlist, saved_comps, saved_searchessamenew
/app/alertsbuyer, investorAlert rules: new DLD sale, price band, handover % change, new launch by a developer. Channels: email, WhatsApp.saved_searchesalertsnew
/app/deals, /app/deals/{id}buyer (own)Deal tracker: stage timeline, next step, docs to upload, fees duedeals, deal_stage_history, deal_docsdeal_docs (upload)new
/investor/portfolioinvestorOwned units: comps value band, yield, handover %, service chargeholdings ⋈ mv_*holdingsnew
/agent/pipelineagentKanban by stage (§10), SLA clocks, filters by source pagedeals (assigned)stage movesnew
/agent/leads, /agent/viewings, /agent/commissionagentInbox, viewing calendar (Google Calendar sync), commission ledgerleads, viewings, commissionssamenew
/ops/sla, /ops/attribution, /ops/ingestops, adminBreach board, page → lead → deal attribution, data freshnessallconfignew

§10Deal management: stages, events, SLA timers

CodeStageEntry eventRequired docs (deal_docs.type)SLA default (sla_rules)Buyer sees
L0New leadlead_created (form, WhatsApp, viewing request, brochure)—First response ≤5 min (09:00–21:00 GST), ≤60 min otherwise“Agent assigned: name, BRN”
L1Qualifiedlead_qualified (budget, cash/mortgage, timeline, GV intent)mortgage_preapproval (if financed)Qualify ≤24 h after L0Shortlist (saved comps shared)
V1Viewing bookedviewing_booked—Book ≤48 h after L1Date, address, agent
V2Viewedviewing_done—Follow-up ≤24 hFeedback form
O1Offer madeoffer_submitted (AED, terms)form_b (buyer–agent agreement)Seller answer chased at 24 h; offer expires at 48 hOffer vs DLD comps band
O2Offer acceptedoffer_accepted—MOU signed ≤3 daysNext: MOU
M1MOU signed + depositmou_signedform_f_mou, deposit_cheque (typically 10%), passport, emirates_id (if resident)NOC applied ≤2 daysFees due (M-TXN calc)
N1Developer NOCnoc_issueddeveloper_nocTarget ≤7 days from M1NOC status
F1Mortgage final offer (if financed)mortgage_final_offervaluation_report, final_offer_letterTarget ≤14 days from M1Bank, LTV, rate
T1Transfer bookedtransfer_booked (trustee centre slot)managers_chequesBook ≤3 days after N1/F1Date, centre, cheques list
T2Transferredtransfer_donetitle_deedMOU → transfer ≤30 days (cash) / ≤45 (mortgage)Title deed copy
C1Closed: commission receivedcommission_paidinvoiceInvoice ≤2 days after T2Post-sale offers (rental, snagging)
Off-plan primary: R1 EOI/reservation → S1 SPA signed → Q1 Oqood registered (oqood_certificate) → P* payment milestones from payment_plans → H1 handover. Off-plan resale: M1 → developer NOC (check the minimum % paid in payment_plans) → Oqood transfer. Lost: X with a required lost_reason (price, finance, timing, competitor, unresponsive).

Events (events table: one row per action, append-only)

page_view, hook_click, sign_up, project_saved, comps_saved, search_saved, alert_sent, alert_clicked, brochure_download, calc_run, whatsapp_click, lead_created, lead_qualified, lead_assigned, viewing_booked, viewing_done, offer_submitted, offer_accepted, mou_signed, noc_issued, mortgage_final_offer, transfer_booked, transfer_done, commission_paid, deal_lost, sla_started, sla_breached, doc_uploaded, doc_verified

Columns: event_id, ts, user_id?, anon_id, session_id, event, entity_type, entity_id, deal_id?, props jsonb, utm jsonb, page_path.

CRM hooks + attribution

  • System of record is the Postgres deals table. Outbound webhooks (crm_webhooks) on stage change mirror to Airtable now, and HubSpot later if needed.
  • WhatsApp Business API: an inbound message creates lead_created, with the entity parsed from the prefilled text (“Re: /projects/2621-eywa-dubai”).
  • Attribution: touches(lead_id, ts, page_path, entity, utm, kind). Reports first touch, last touch and assisted touches, plus the page template that sourced the lead.
  • Agent assignment: round robin within the area specialism. A lead is reassigned if the L0 SLA is breached twice. Split rules (Form A/B/I) live in commissions.
  • SLA engine: sla_timers(deal_id, stage, due_at, breached_at). A cron runs every 5 min. On breach it pushes to the agent, then escalates to ops at 2× the SLA.

§11DB tables (Postgres + PostGIS; Supabase auth + RLS)

Reference / public dataKey columns
dld_transactionstransaction_id PK, date, trans_group, procedure, reg_type, property_type, sub_type, usage, area, master_project, project, project_no, building, rooms, parking, size_sqm, amount_aed, aed_per_sqm, nearest_metro, nearest_mall, nearest_landmark, excluded_reason
dld_rentscontract_id, start_date, end_date, area_id, project, beds, size_sqm, annual_aed
dld_projectsproject_no PK, name, dev_id, mp_id, area_id, status, pct_complete, inspected_on, planned_completion, actual_completion, escrow_no, plot_sqm, floors, units, mix_json, lat, lon, geo_source
developers, master_projects, areas, buildings, aliasesslug UNIQUE, DLD ids, names, geo
pf_matchesdld_tx_id, unit_key, unit_no, floor, pf_building_url, confidence real, history_n, history_seq, kpi_scope, flag
schools, pois, osm_buildings, geo_shapesids, category, geom
drive_times, entity_distances, entity_schools, entity_poi_countsentity_type, entity_id, target, value, computed_at
service_charges, payment_plans, source_conflictsentity, year/milestone, value, source_url
ref_fees, ref_ltv_rules, ref_rates, ref_golden_visa_rules, ref_process_steps, ref_fxEvery row has value, source_url and checked_at. A stale banner appears after 90 days.
mv_project_kpis, mv_project_quarterly, mv_area_monthly, mv_dev_sales_l12m, mv_rent_kpis, mv_supply, mv_comps, mv_unit_historyMaterialised views, refreshed after each ingest
assets, ad_permits, page_inventory§8; Trakheesi; generator inventory (url, template, entity, lastmod, status)
Portal / CRM (RLS on)Key columns
profilesuser_id PK, role (buyer|investor|agent|ops|admin), name, phone, country, consent_marketing, consent_ts
agentsuser_id, brn, orn, company, areas[], active, verified_at
watchlist, saved_comps, saved_searches, scenariosuser_id, entity / tx_ids[] / filter jsonb / inputs jsonb, shared_with_agent
alertsalert_id, search_id, trigger_tx_id, channel, sent_at, clicked_at
holdingsuser_id, unit_key / project_no, purchase_date, purchase_aed
leadslead_id, user_id?, name, phone, email, source(form|whatsapp|call), entity_type, entity_id, unit_key?, utm, assigned_agent, created_at
dealsdeal_id, lead_id, buyer_id, agent_id, path(ready_cash|ready_mortgage|offplan_primary|offplan_resale), entity, unit_key, stage, offer_aed, agreed_aed, expected_close, lost_reason
deal_stage_historydeal_id, from_stage, to_stage, ts, by_user
deal_docsdoc_id, deal_id, type, r2_key (private bucket dcu-deals), uploaded_by, verified_by, expires_at
viewings, offers, commissionsdeal_id, times / amounts / split rules (Form A/B/I)
touches, events, sla_rules, sla_timers, crm_webhooks§10

§12Who sees what (RLS matrix)

Objectanonbuyerinvestoragentopsadmin
Public data (DLD, overlays, ref_*)readreadreadreadreadwrite
watchlist / saved_* / scenarios—ownownrows shared with themreadall
holdings——ownif sharedreadall
leadsinsert onlyown (read)ownassignedallall
deals + stage history—own (read)ownassigned (write)allall
deal_docs (private bucket, 10-min signed URLs)—own (upload/read)ownassigned (verify)readall
commissions———ownreadall
events / touchesinsertinsertinsertread (own deals)readall
assets (unpublished, rights)published only+ gated+ gated+ gatedallall

PII never appears in public HTML, CSVs or llms.txt. DLD rows carry no owner names. UK GDPR and UAE PDPL apply: consent is captured at sign-up, and export/delete lives at /app/account.

§13AIO / schema / open data (per template)

TemplateJSON-LDRaw HTML must containOpen data twin
T-DEVOrganization (identifier = DLD dev no.) + ItemList of projects + BreadcrumbListProjects table with % complete and median psf/data/developers/{slug}.csv
T-MP / T-AREAPlace + GeoShape + Dataset (monthly series) + BreadcrumbList36-month median psf and volume table/data/areas/{slug}-monthly.csv
T-PRJApartmentComplex (geo, numberOfAccommodationUnits) + Dataset (transactions) + DigitalDocument/VideoObject/ImageObject + BreadcrumbList. RealEstateListing only when a live permitted offer exists.KPI tiles as text, quarterly table, full transaction table/data/projects/{no}.csv
T-BLDApartmentComplex + DatasetUnit table, floor-band table/data/buildings/{slug}.csv
GuidesHowTo / FAQPage / GovernmentService refFee and rule tables with source and checked_at/data/ref/{table}.csv
Sitesitemap.xml index → sitemap-{template}.xml (lastmod = data refresh). llms.txt + llms-full.txt (template list, dataset links, method). robots.txt allows all public routes and /data/, and disallows /app /agent /ops /investor. Bulk Parquet: /data/dld-transactions.parquet (DLD open data, attribution kept).

§14Sprint backlog: shippable tickets

Build preview host: dubai-co-uk.articulate-ai.work (cwdeploy). Moves to the dubai.co.uk apex only after the ownership check in §17.

Week 1 (Mon 5 – Sun 11 Oct): data spine + EYWA vertical slice

TicketDeliverableAcceptance (testable)
DCU-101Next.js (App Router, SSR) repo + Vercel project dubai-co-uk; layout, breadcrumbs, source-tag componentPreview URL returns 200. curl | grep finds the page H1 without JS.
DCU-102Postgres + PostGIS schema v1 (reference tables, §11). Loader from transactions.parquet and sales_extended.parquetRow counts = 1,661,875 / 1,364,266 (meta.json). Loader is idempotent.
DCU-103dld_projects ingest (status, % complete, planned completion, escrow) for the 420 inventory projects#2621 → 64.2%, inspected 2026-09-17, planned 2026-04-01, escrow 200974659
DCU-104pf_matches loader (batch 1) + exclusions + flags2,964 tx / 2,332 units. EYWA plot 57 excluded. PF units 503/604 flagged under #3890 and kept out of #2621 KPIs.
DCU-105mv_project_kpis + mv_project_quarterly (unit sales only, no lease-to-own, outlier band 1/3×–3× building median)#2621: n=15, median 3,906 AED/sqft, median price AED 17,096,094, last sale 2026-04-01
DCU-106T-PRJ template (identity, KPIs, quarterly, transactions, M-CONF) + /data/projects/{no}.csvThe EYWA wire in §16 renders. CSV rows = HTML rows. Lighthouse SEO ≥ 95.
DCU-107JSON-LD pack v1 (BreadcrumbList, ApartmentComplex, Dataset)schema.org validator: 0 errors on EYWA
DCU-108Supabase auth + profiles roles + RLS skeleton; lead CTA (form + WhatsApp deep link) → leads + eventsAnon select * from leads is denied. A test lead from EYWA lands with entity_type=project, entity_id=2621 and utm, and the agent is notified.

Week 2 (Mon 12 – Sun 18 Oct): all templates + overlays + 1,000-page generator

TicketDeliverableAcceptance
DCU-201T-DEV + dev_scores/developers/r-evolution lists #2621 with the overdue flag. On-time % is hidden when n<3, with the reason shown.
DCU-202T-MP + T-AREA (+ DLD area aliasing, e.g. Burj Khalifa ↔ Business Bay)/areas/business-bay 36-month table matches a SQL spot check of 3 random months
DCU-203T-BLD + floor-band + unit tableUnit table uses matched pairs only. The 81 rows with no PF floor show blank, not a derived value.
DCU-204M-LOC + M-SHP: Geofabrik GCC extract → pois; PMTiles static mapEYWA shows Burj Khalifa at 1,560 m. POI counts are non-zero for every inventory project.
DCU-205M-TRF: OSRM container, 8 anchors, Salik gatesdrive_times has 8 rows per inventory entity, and the “free-flow” label is visible
DCU-206M-SCH: KHDA ingest + geocode≥90% of KHDA schools geocoded. EYWA shows 8 schools with rating year.
DCU-207M-FIN, M-GV, M-TXN + ref_* tables with source_url / checked_at; /guides/{golden-visa-property|buying-process|mortgages|fees}The EYWA worked example equals the §16 numbers. Any ref row older than 90 days shows a stale banner.
DCU-208M-YLD (Ejari rents) + M-SUPEvery yield cell shows n. Benchmark rows name their source buildings.
DCU-209Generator: page_inventory → 1,000 URLs, sitemap index, llms.txt, robots≥900 URLs return 200, and each has ≥1 <table> with numbers in raw HTML
DCU-210Assets: R2 dcu-assets, assets table, assets-ingest CLI; EYWA brochure + floorplansM-CONF shows “Brochure says 48 residences” next to “DLD: 53 units”. The PDF is served from the CDN.

Week 3 (Mon 19 – Sun 25 Oct): portal tools + deal ops

TicketDeliverableAcceptance
DCU-301/map on our mv_* (port the dubai-prices prototype)Deep link /map?project=2621 opens on EYWA. No third-party figures.
DCU-302/dld browser port + “save comps”Filter → save → the set appears in /app/saved and can be shared with the agent
DCU-303/app, /app/saved, watchlist, saved searchesA signed-in user saves EYWA from the project page and it appears on /app immediately
DCU-304Alerts job: nightly DLD delta → matching saved_searches → email/WhatsAppA synthetic new #2621 sale triggers an alert within one job run. The click is tracked as alert_clicked.
DCU-305Deals: deals, stage history, docs (private R2, signed URLs), /agent/pipeline kanban, /app/deals/{id}The buyer sees only their own deal. The agent moves it L0→M1. The required-doc checklist blocks M1 without form_f_mou.
DCU-306SLA engine (cron 5 min) + /ops/slaA lead left unanswered for 6 min in business hours creates sla_breached and an agent push
DCU-307Attribution (touches) + /ops/attribution; Airtable mirror webhookA report shows leads by template (T-PRJ/T-AREA/…) and first/last touch. The Airtable row updates on stage change.
DCU-308M-TRUST (BRN/ORN/Trakheesi) + QA pass: Screaming Frog, raw-HTML number audit (50 pages), CWV4xx <1%. 50/50 pages show KPIs without JS. LCP <2.5 s on T-PRJ.

§15Acceptance criteria (global, checked by CI per deploy)

#CheckHowPass
A1Numbers visible without JScurl -s URL | grep -c 'class="n"' on a 50-URL sample≥1 per page, 100% of the sample
A2CSV parityDiff the HTML table against /data/…csv0 diffs
A3ProvenanceEvery KPI node has a data-src attribute and a visible source tag100%
A4Small-n honestyNo median where n<3. YoY needs n≥5 in both windows.0 violations
A5Schema validschema.org validator on 1 URL per template0 errors
A6Portal hook presentDOM has ≥1 [data-hook] per public page100%
A7RLSpgTAP tests per role for §12All pass
A8No competitor dataThe source enum excludes dxbinteract; build grep0 hits
A9FreshnessDLD ingest age; ref_* checked_at≤7 d; ≤90 d

§16Sample page wire: EYWA (dubai.co.uk/projects/2621-eywa-dubai)

All figures below are real values from our current extract: DLD sales, the PF batch-1 matches (14 of 15 sales matched to a unit) and the EYWA spec (context_kpis.json, generated 2026-10-05 08:09 GST). Finance rows apply the M-FIN rules with a [A] rate.

┌─ dubai.co.uk ─ Developers · Areas · Projects · Map · DLD · Guides ─────────────────── [Sign in] [WhatsApp] ┐
│ Home › Business Bay (master) › Business Bay (area) › EYWA Dubai                                              │
│ EYWA Dubai (Tree of Life)                        DLD #2621 "EYWA DUBAI" · R.Evolution (RVL Real Estate LLC) │
│ Marasi Drive, Business Bay · on the Dubai Water Canal · 25.183098, 55.273213            [★ Save] [🔔 Alert] │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ STATUS  Off-plan · 64.2% complete (DLD inspection 17 Sep 2026) · Planned handover 1 Apr 2026  ⚠ OVERDUE      │
│         Escrow 200974659 · Plot 3,463.67 m² · 25 floors · 53 units (19×2BR · 22×3BR · 6×4BR · 6×5BR)       │
├─ KPI TILES  segment Residential · DLD unit sales 2020 → 3 Oct 2026 · lease-to-own + outliers excluded  [D] ─┤
│  Sales 15 │ Median 3,906 AED/sqft │ Median price AED 17,096,094 (+GBP, ref_fx date) │ Median size 3,693 sqft │
│  First sale 16 Feb 2024 │ Last sale 1 Apr 2026: AED 17,403,846 @ 4,915/sqft │ Sales L12M 1 (1.9% of units)    │
│  By beds: 2BR 3,860 (n=10) · 3BR 4,143 (n=5) │ YoY: not shown (n<5 in a window) │ Sales/yr 2024 10 · 2025 4 · 2026 1│
├─ QUARTERLY median AED/sqft (table + chart) ──────────────────────────────────────────────────────────────────┤
│  2024Q1 3,792 (n3) · 2024Q2 3,854 (n4) · 2024Q3 n=1: 3,828 · 2024Q4 4,466 (n2)                             │
│  2025Q2 4,686 (n2) · 2025Q3 3,854 (n2) · 2026Q2 n=1: 4,915                       [Open /dld?project=2621]  │
├─ ALL TRANSACTIONS  SSR table · 15 rows + "Excluded (6)" ─────────────────────────────────────────────────────┤
│  Date        Procedure                Beds  Sqft    AED          AED/sqft  Unit  Floor  Match conf          │
│  2026-04-01  Sell - Pre registration  2BR   3,541   17,403,846   4,915     402   4      PF 1.00             │
│  2025-08-13  Sell - Pre registration  3BR   5,223   19,150,000   3,667     501   5      PF 0.93             │
│  2025-07-15  Sell - Pre registration  2BR   2,945   11,900,000   4,040     803   8      PF 0.93             │
│  2025-06-19  Sell - Pre registration  2BR   3,756   14,325,300   3,813     203   2      PF 0.93             │
│  2025-06-03  Sell - Pre registration  2BR   3,674   20,429,550   5,560     —     —      unmatched           │
│  2024-12-18  Sell - Pre registration  3BR   6,466   26,788,960   4,143     802   8      PF 1.00             │
│  … 9 more (2024)                                                                       [Save these comps]   │
│  Excluded: land plot sales 2013/2016/2021/2022 (e.g. 2022 AED 74,285,000) · 2026-08-27 AED 12M + 2026-09-02 │
│  AED 8M filed under DLD #3890 "Eywa The Way Of Water" (sizes don't match units 503/604) · reason shown     │
├─ ⚠ SOURCE CONFLICTS (M-CONF) ────────────────────────────────────────────────────────────────────────────────┤
│  Units/storeys: DLD 53 / 25 floors (USED) · Brochure "48 exclusive residences" · Christie's 48 / 20 storeys │
│  · datahabibi 58 / 23 storeys · Shababeek G+3P+19                                                            │
├─ RENT & YIELD (M-YLD) ──────────────────────────────────────────────────────────────────────────────────────┤
│  No Ejari contracts (off-plan). Benchmark [B]: 237 AED/sqft/yr median of 44 PF-published rents ≥2,000 sqft  │
│  (Volante · Dorchester Collection · Jumeirah Living BB) → indicative gross yield 6.1% at 3,906/sqft         │
├─ GOLDEN VISA (M-GV) ─────────────────────────────────────────────────────────────────────────────────────────┤
│  15 of 15 recorded unit sales ≥ AED 2M → every unit sold here clears the property-value threshold.          │
│  Checklist ▸ value · Oqood/title deed · mortgage conditions · joint owners       [/guides/golden-visa-property]│
├─ FINANCE at median price AED 17,096,094 (M-FIN) ─────────────────────────────────────────────────────────────┤
│  Off-plan, 50% LTV cap  → loan 8,548,047 · equity 8,548,047 · 25y @ 4.49% [A] → AED 47,464 / month          │
│  At handover, expat >AED 5M, 70% LTV → loan 11,967,266 · deposit 5,128,828 → AED 66,450 / month            │
│  Cash-to-close (70% case): deposit 5,128,828 + DLD 4% 683,844 + trustee 4,200 + agent 2%+VAT 359,018         │
│   + mortgage reg 0.25%+AED 290 = 30,208  →  AED 6,206,098 (excl. bank arrangement/valuation)   [/calc/cost] │
├─ PROCESS (M-TXN, path = offplan_resale) ─────────────────────────────────────────────────────────────────────┤
│  1 Offer → 2 MOU (Form F) + deposit → 3 Developer NOC (min % paid check) → 4 Oqood transfer at trustee     │
│  → 5 Handover + snagging · fees computed above                                [Start this with an agent]  │
├─ LOCATION · COMMUTE · SCHOOLS · SHOPPING (M-LOC / M-TRF / M-SCH / M-SHP) ───────────────────────────────────┤
│  Burj Khalifa 1,560 m (bearing 3°) · canal frontage north · nearest metro (DLD field) ·                     │
│  DIFC / Downtown / DXB / DWC / Marina / Internet City / Dubai Hills: km · free-flow min · Salik gates     │
│  8 KHDA schools ≤5 km (rating · year · fees AED) · POI counts 1 km / 3 km (mall, supermarket, clinic …)   │
├─ MEDIA (§8) ── Brochure PDF "Eywa Offer 203" · Floorplans (2BR Type B …) · Payment plan · Developer video ──┤
│   · Gallery · 3D tower viewer (eywa.articulate-ai.work)       "Brochure says" table next to the DLD values  │
├─ COMPS 500 m ── Volante · Volante 2 · Dorchester Collection · Jumeirah Living BB …  [Compare] [Save comps] ┤
├─ CTA ── [Book viewing (UK-time video slots)] [WhatsApp "Re: /projects/2621-eywa-dubai"]                    │
│   Agent BRN · ORN · Trakheesi no. (M-TRUST) · signed-in with open deal → "O1 Offer made · 2 d · next: MOU" │
├─ Open data: /data/projects/2621.csv · Method: /method · source tag on every figure ─────────────────────────┤
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

§17Domain + deploy status (checked 5 Oct 2026, GST)

ItemStateAction
dubai.co.uk apexA → 216.150.1.1 (Vercel edge). HTTPS returns 403 x-vercel-mitigated: deny. Not in the Articulate-AI Vercel team domains. The Hostinger portfolio check timed out. www does not resolve.Confirm ownership (Hostinger / registrar) before promotion in DCU-308
Plan (this page)dubai-co-uk-plan.articulate-ai.work, deployed with cwdeploy from MacProVault: Articulate/Dubai-co-uk/plan/
Build previewdubai-co-uk.articulate-ai.work (created in DCU-101)cwdeploy. Never here.now.
Existing assets reuseddubai-prices (map prototype) · eywa (3D) · DLD-Browser/dld-data (Parquet, 1.66M tx) · DLD-Browser/matches (PF batch 1)Ported in W1 / W3