Back to BlogIndustry Guides

Sales Inventory CRM for Off-Plan Real Estate Developers

Rupak Amin

Founder & Lead Engineer, RAITHub

14 min read

A sales inventory CRM for an off-plan developer holds every unit as one record with one status, lets staff and brokers place expiring holds that can never double-book, stores each payment plan as instalment rows, and tracks the documents each sale needs. In Dubai, off-plan projects are registered with DLD alongside an escrow account. BlockEstate, RAITHub's multi-tenant listing platform with a document workflow, reached MVP in 6 weeks.

This guide is for sales directors, CTOs and founders at real estate developers, in the Gulf, South Asia, Africa or elsewhere, whose off-plan inventory still lives in spreadsheets shared between the sales team and a dozen broker agencies. It shows the data model and the concurrency rule that stop a unit being sold twice. RAITHub is a software studio in Dhaka, Bangladesh. BlockEstate, its PropTech listing platform, has inquiries, lead routing and a document workflow, but it is not a developer sales system, so the inventory, hold and payment-plan patterns below are engineering guidance rather than a shipped product. Anything about escrow, registration or sales regulation is general information, not legal advice; confirm the current rule with your adviser.

Why do developers outgrow spreadsheets for off-plan inventory?

Because a spreadsheet cannot enforce that a unit has exactly one owner at a time. At a project launch, several people try to reserve the same units within minutes of each other. Whoever saves last wins, and the developer finds out when two brokers each tell a buyer that unit 1204 is theirs.

The other failures follow the same pattern:

  • Stale availability. Brokers work from a PDF sent last week, so they pitch units that have gone.
  • Holds that never expire. A broker blocks ten units "for a client" and nobody releases them.
  • Payment plans typed by hand. Each sale's instalments are retyped into a new sheet, with rounding errors and wrong due dates.
  • Documents in inboxes. The reservation form, ID copies and signed sale agreement sit in different email threads.
  • No audit trail. When a price or status changes, nobody can say who changed it or why.

What should a real estate developer CRM track?

Six things, each as its own table, linked to the unit. Getting these entities right matters more than any screen.

EntityWhat it holdsRule that matters
Project and phaseTowers, phases, launch dates, registration and escrow referencesA phase has one active price list at a time
UnitCode, type, floor, area, view, list price, statusOne status per unit: available, held, reserved, sold or blocked
HoldWho holds a unit, for which buyer, until whenAt most one live hold per unit, and every hold expires
SaleBuyer, unit, agreed price, payment plan, broker, stagePrice is fixed at booking; later price-list changes do not touch it
InstalmentEach amount due, its trigger, due date and payment statusInstalments add up exactly to the agreed price
DocumentType, version, status, file, who uploaded and approved itA sale cannot move stage until its required documents are approved

Inquiries and leads sit in front of this, from the website, portals and brokers. Lead capture and agent tooling are covered in building a real estate CRM for agents; this post covers what happens once a buyer wants a specific unit.

How do you stop two brokers reserving the same unit?

Lock the unit's row inside a database transaction before checking its status, and back that up with a unique index so only one live hold can ever exist. Checking status in application code and then writing is not enough: two requests can both read "available" before either writes.

PostgreSQL's FOR UPDATE "causes the rows retrieved by the SELECT statement to be locked as though for update. This prevents them from being locked, modified or deleted by other transactions until the current transaction ends" (PostgreSQL: explicit locking). The second request waits, then sees the unit as held.

create table units (
  id               uuid primary key default gen_random_uuid(),
  project_id       uuid not null,
  code             text not null,                  -- 'T2-1204'
  status           text not null default 'available'
    check (status in ('available', 'held', 'reserved', 'sold', 'blocked')),
  list_price_minor bigint not null,                -- fils, never floats
  unique (project_id, code)
);

create table holds (
  id          uuid primary key default gen_random_uuid(),
  unit_id     uuid not null references units(id),
  broker_id   uuid,                                -- null for in-house sales
  held_by     uuid not null,
  expires_at  timestamptz not null,
  released_at timestamptz
);

-- Belt and braces: the database refuses a second live hold on a unit.
create unique index one_live_hold_per_unit on holds (unit_id) where released_at is null;

The hold itself, with node-postgres:

import type { PoolClient } from 'pg'

export async function placeHold(
  client: PoolClient, unitId: string, brokerId: string | null, userId: string, hours: number,
) {
  await client.query('begin')
  try {
    // A second request for this unit waits here until we commit or roll back.
    const unit = await client.query('select status from units where id = $1 for update', [unitId])
    if (unit.rowCount === 0) throw new Error('unit not found')

    // Release this unit's hold if it has expired, so the unit can be held again.
    const expired = await client.query(
      'update holds set released_at = now() where unit_id = $1 and released_at is null and expires_at <= now()',
      [unitId],
    )
    let status: string = unit.rows[0].status
    if (status === 'held' && (expired.rowCount ?? 0) > 0) status = 'available'

    if (status !== 'available') {
      await client.query('rollback')
      return { ok: false as const, reason: 'unit is ' + status }
    }

    await client.query("update units set status = 'held' where id = $1", [unitId])
    await client.query(
      'insert into holds (unit_id, broker_id, held_by, expires_at) values ($1, $2, $3, now() + make_interval(hours => $4))',
      [unitId, brokerId, userId, hours],
    )
    await client.query('commit')
    return { ok: true as const }
  } catch (err) {
    await client.query('rollback')
    throw err
  }
}

Three rules around it. The hold length is sales policy, set per project, not hard-coded. A scheduled job releases expired holds every few minutes, so the availability board is accurate even when nobody tries to re-hold a unit. And the same lock is taken when a hold is converted to a reservation, so a hold cannot expire halfway through a booking. The general pattern, applied to e-commerce stock, is in how to prevent overselling inventory.

How do you model off-plan payment plans as data?

As a template of steps per project, copied into instalment rows when a sale is booked. The template describes the plan; the instalments are the buyer's actual schedule, and they never change when the template does.

A step has a share of the price and a trigger: on booking, a number of days after booking, a construction milestone, or handover. The plan below is illustrative, to show the shape, not a market norm:

StepShareTriggerDue date known at booking?
Booking10%On bookingYes
Second instalment10%90 days after bookingYes
Construction milestones40% across several stepsEach milestone certifiedNo, set when certified
Handover40%On handoverNo
create table plan_steps (
  template_id uuid not null,
  seq         int  not null,
  label       text not null,
  share_bp    int  not null check (share_bp between 1 and 10000),  -- basis points
  due_on      text not null check (due_on in ('booking', 'days_after_booking', 'milestone', 'handover')),
  days        int,
  milestone   text,
  primary key (template_id, seq)
);

create table instalments (
  id           uuid primary key default gen_random_uuid(),
  sale_id      uuid not null,
  seq          int  not null,
  label        text not null,
  amount_minor bigint not null check (amount_minor > 0),
  due_date     date,                 -- null until the milestone is certified
  status       text not null default 'scheduled'
    check (status in ('scheduled', 'due', 'paid', 'overdue', 'waived')),
  unique (sale_id, seq)
);

Two details prevent most disputes. Store shares in basis points and amounts in minor units, round each instalment down, and put the remainder on the last one, so the schedule adds up exactly to the agreed price. And check that a template's steps total 10,000 basis points before it can be used; a CHECK constraint cannot sum across rows, so do it in a trigger or in the publish step for the template, with a test.

In Dubai, where the money goes is regulated. DLD's project registration service "enables real estate development companies to register a real estate project and open an escrow account for off-plan sales", with a 30% construction, guarantee or deposit requirement and a listed fee of AED 150,000 (DLD: Register Project). The DLD API gateway also lists a Trust Account Bulk Inflow API that lets registered developers submit property payment records to the Trust Account System in bulk, up to 20 records per upload (DLD API Gateway). A CRM should therefore record, per instalment, which account it is due into and its reconciliation state. Which payments must go where is a question for your adviser and your escrow bank, not your software vendor.

How should broker allocation and permissions work?

Treat each broker agency as a tenant with its own users, and control which units it can see and hold through data, not through separate spreadsheets. Most developers want some mix of three policies, often varying by launch phase:

  • Open pool: every approved broker sees all available units, and the first valid hold wins.
  • Allocated blocks: a broker agency gets a set of units for a period; only it can hold them until the allocation ends.
  • In-house first: units open to brokers only after an internal sales window.

Store allocations as rows (unit, broker agency, from, until) and check them inside the same transaction as the hold. Broker users should see their own agency's holds, buyers and commissions, and never another agency's; that is the same isolation problem as any multi-tenant product, covered in the Postgres row-level security guide. Roles and permission codes, such as "can hold", "can extend a hold" and "can approve a discount", are designed in SaaS authorization and RBAC design.

Commission is data too: a rate per project or broker tier, and a rule for when it becomes payable, for example after the buyer has paid a set share of the price. In Dubai, DLD's Dubai Brokers API gives subscribers with an active DLD business account real-time broker card and broker office information, which can verify a broker at onboarding (DLD API Gateway).

What does the document workflow for an off-plan sale look like?

A checklist of required documents per sale stage, each document with a status and version, and a rule that the sale cannot advance until the current stage's documents are approved.

Sale stageTypical documentsMoves on when
HoldNone, or a buyer nameBooking payment received
ReservationReservation form, buyer ID documents, booking receiptDocuments approved by sales admin
AgreementSale and purchase agreement, signed by both partiesSigned copy uploaded and checked
RegistrationThe registration your market requires; in Dubai, ask your adviser how the sale is registered in DLD's Oqood systemRegistration reference recorded
PaymentsReceipts per instalmentEach instalment reconciled
HandoverCompletion and handover documentsFinal instalment paid

Store files in private object storage, served through short-lived signed URLs, and record every upload, approval and download in an append-only audit log. Buyer ID documents are personal data, so decide where they are hosted before you build; RAITHub signs DPAs and SCCs and follows your controls. The audit design is in SaaS audit log design. Which documents your market requires, and in what order, is for your adviser to confirm.

Off-the-shelf or custom CRM for a real estate developer?

Off-the-shelf when your sales process fits the product; custom when inventory, holds, payment plans and broker rules are the process. Many developers do both: a general CRM for marketing and leads, and a custom inventory and sales core connected to it.

SituationLean towardsWhy
One project, under a few hundred units, in-house sales onlyAn off-the-shelf CRM and a well-kept sheetConcurrency problems are rare at that scale
Several projects, many broker agencies, launch-day rushesCustom inventory and sales coreHolds and allocations are where double-selling happens
Payment plans vary by project and are renegotiatedCustomPlans as data beat plans as spreadsheets
Marketing, email campaigns and lead scoringOff-the-shelfMature products already do this well
Regulator or bank integrations your licensed entity subscribes toCustom adapterThe integration follows your process, not a vendor's roadmap

On cost, published 2026 market guides put a basic real estate CRM at $10,000–$25,000 and a medium one with automation and integrations at $30,000–$75,000, as summarised in the cost to build a real estate platform. Those are market figures, not RAITHub quotes. The MVP cost estimator gives a quick range for your scope.

Why RAITHub for this?

Because the hard parts of a developer sales system, concurrency, tenant isolation, permissions and document workflow, are the parts RAITHub's shipped platforms exercise.

  • BlockEstate: a multi-tenant listing and inquiry platform with each brokerage isolated as a tenant, agent dashboards, lead routing and a document workflow. MVP in 6 weeks. It does not include unit inventory, holds or payment plans; those would be new work. See the BlockEstate case study.
  • Permissions at depth: Sundor Skin, a B2B wholesale platform RAITHub built, has 146 PostgreSQL tables with row-level security, 88 permission codes, 12 staff roles, credit terms and 530+ automated tests.
  • Money on a schedule: PropDesk, a property management platform, collects rent through Stripe, runs 5 scheduled jobs and has 1,024 automated tests. Overdue instalments and hold expiry are the same kind of job.
  • Working hours. Dhaka is UTC+6: about 7 shared working hours with Dubai and about 3 to 4 with the UK. The working week is agreed per client, with written daily handoffs.
  • How it is engaged. Fixed-scope builds or a monthly dedicated team under SaaS development, with integrations under API and backend development. You own the code, and an NDA is standard.

When you don't need us

  • You sell one building with an in-house team. A general CRM and a shared sheet with one owner will do.
  • You need a vendor with a shipped Oqood, escrow or DLD integration. RAITHub has not built one, and the subscription belongs to your licensed entity.
  • You need Arabic-language delivery or an on-site team. RAITHub builds right-to-left interfaces but delivers in English only, and has no office in the UAE or elsewhere outside Dhaka.
  • You need native mobile apps for brokers. RAITHub builds web applications.
  • You need advice on escrow, registration or sales law. That is for a qualified adviser.

The real estate industry page covers RAITHub's PropTech work more broadly. If your inventory has outgrown its spreadsheet, book the free 15-minute technical audit and bring a sample of your unit sheet, one payment plan and how brokers get units today.

DLD and PostgreSQL sources checked on 30 September 2026.

Frequently asked questions

What is a real estate developer sales inventory CRM?

A system that holds every unit in a project with one live status, manages holds and reservations by staff and brokers, turns payment plans into instalment schedules, and tracks the documents each sale needs, with an audit trail of every change.

How do you prevent double-booking a unit at a launch?

Lock the unit row with SELECT ... FOR UPDATE inside a transaction before checking its status, and add a unique index that allows only one live hold per unit. The second request waits, then sees the unit is already held.

How should holds on off-plan units expire?

Give every hold an expiry time set by project policy, release expired holds with a scheduled job, and re-check expiry inside the locked transaction whenever someone tries to hold or book the unit.

How do you store an off-plan payment plan in software?

As a project template of steps, each with a share in basis points and a trigger such as booking, a date, a milestone or handover. At booking, copy it into instalment rows in minor units, with any rounding remainder on the last instalment.

Can brokers see only their own units and buyers?

Yes, if each broker agency is a tenant and the database enforces it with row-level security, with unit allocations stored as rows and checked inside the hold transaction.

Does an off-plan sales system in Dubai need to handle escrow?

DLD registers off-plan projects alongside an escrow account, and its API gateway lists a bulk payment-record API for registered developers. Record which account each instalment is due into, and confirm the rules with your adviser and escrow bank.

Has RAITHub built a developer sales CRM before?

Not as such. BlockEstate covers multi-tenant listings, inquiries, lead routing and a document workflow; unit inventory, holds and payment plans would be scoped as new work after a free 15-minute technical audit.

real estate inventory management software dubaibuilder crm softwareoff-plan sales crmreal estate developer crmunit reservation systempayment plan softwareproptech

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.