Skip to content

Building a real estate marketplace with Next.js, WebSockets, and a Heygen AI avatar

A technical walkthrough of architecting a real estate marketplace on Next.js with real-time property updates, Mapbox geospatial search, and a Heygen-powered AI avatar assistant that guides buyers through listings.

12 min read
By Digitizia

Real estate marketplaces look simple from the outside: a property grid, a map, filters, detail pages. Inside, they are one of the harder things you can build on the web. Listings change in real time. Buyers expect instant filtering and geo search. Sellers want analytics. And in 2026, the bar has moved again — buyers now expect an interactive AI guide that can answer questions about properties the way a human agent would.

At Digitizia, we delivered a real estate marketplace for the Dubai market that brought all of these pieces together: Next.js, role-based dashboards, WebSockets for real-time listing updates, Mapbox for geospatial search, and a Heygen-powered AI avatar assistant. The platform was architected for 10,000+ concurrent users. This post walks through the architecture decisions, the tricky parts, and how we would approach the same build today.

The brief

Build a marketplace where agents list properties, buyers search and filter, and everyone can interact with an AI avatar that helps answer questions in natural language. Must handle thousands of active users browsing at the same time, update new listings instantly, and feel fast on mobile networks across the Middle East.

Core requirements we locked before writing any code:

  • Role-based dashboards (buyer, seller, admin)
  • Real-time listing updates without page refresh
  • Map-based search with clustering and filters
  • AI avatar assistant for property questions
  • Mobile-first performance on weak networks
  • Architected for 10K+ concurrent users

The stack

We picked boring, proven tools for the core and put the novelty at the edges where it actually matters.

  • Next.js (App Router) — SSG for static pages, SSR for listing detail, client components where interaction matters
  • Node.js + Express — API layer, WebSocket server, Heygen token issuance
  • MongoDB — listings, users, saved searches, chat history
  • AWS S3 — property images and documents
  • Mapbox GL JS — interactive map, clustering, geospatial queries
  • Socket.io — real-time listing updates and dashboard notifications
  • Heygen Interactive Avatar — embedded AI guide
  • Vercel + CloudFront — global delivery

The architecture

At a high level: Next.js renders pages and hydrates a client that opens a Socket.io connection for live updates. API requests go through a Node.js backend that talks to MongoDB and AWS. A separate WebSocket channel streams listing changes. The Heygen avatar runs in a dedicated client component that requests a short-lived session token from our backend on mount.

Routing pattern

We used Next.js App Router with a mix of static and dynamic rendering:

app/
├── (marketing)/              // SSG — landing, about, pricing
├── listings/
│   ├── page.tsx              // server component, paginated grid
│   ├── [id]/page.tsx         // server-rendered detail + generateMetadata
│   └── map/page.tsx          // client component, Mapbox
├── dashboard/
│   ├── buyer/page.tsx
│   ├── seller/page.tsx
│   └── admin/page.tsx
├── api/
│   ├── listings/route.ts
│   ├── heygen/token/route.ts // issues short-lived Heygen session tokens
│   └── socket/route.ts
└── layout.tsx

The listings grid is a server component that fetches the first page on the server, then hydrates a client component that takes over filtering and pagination. This keeps the first paint fast and search-engine-friendly while giving users a snappy SPA feel after load.

Real-time listing updates

When a seller creates, updates, or marks a property sold, every buyer with a relevant search open should see the change within a second. We used Socket.io rooms keyed by filter signature so each client only receives updates that match what they are looking at.

// server — broadcast a listing change to relevant rooms
io.on("connection", (socket) => {
  socket.on("subscribe", ({ filterKey }) => {
    socket.join(filterKey);
  });
});

// after a listing mutation
export async function broadcastListingChange(listing: Listing) {
  const filterKeys = getMatchingFilterKeys(listing);
  for (const key of filterKeys) {
    io.to(key).emit("listing:update", listing);
  }
}

The client side subscribes on mount and patches the local cache without touching network:

"use client";
import { useEffect } from "react";
import { io } from "socket.io-client";

export function useListingSubscription(filterKey: string, onChange: (l: Listing) => void) {
  useEffect(() => {
    const socket = io("/", { path: "/api/socket" });
    socket.emit("subscribe", { filterKey });
    socket.on("listing:update", onChange);
    return () => {
      socket.disconnect();
    };
  }, [filterKey, onChange]);
}

Geospatial search with Mapbox

Mapbox GL JS gives you an interactive map, but the real work is in clustering and efficient viewport queries. We store listings with a GeoJSON point on each document and use MongoDB's 2dsphere index for $near and $geoWithin queries.

// MongoDB index
db.listings.createIndex({ location: "2dsphere" });

// Query listings inside the current map viewport
const listings = await db.listings.find({
  location: {
    $geoWithin: {
      $box: [
        [viewport.west, viewport.south],
        [viewport.east, viewport.north],
      ],
    },
  },
  status: "active",
}).limit(500).toArray();

For clustering, we let Mapbox handle it on the client side using its built-in supercluster. The server returns points; Mapbox groups them based on zoom level. This pushes rendering work off the main thread and keeps pan/zoom smooth.

Adding the Heygen AI avatar

Heygen's Interactive Avatar API lets you embed a streaming video of an AI character that listens, answers in natural language, and lip-syncs in real time. For a real estate site, it works as a friendly guide — greet new visitors, answer questions about listings, hand off to a human when needed.

Security: never expose your API key

Heygen requires an API key to create sessions. That key must never ship to the browser. Instead, your Next.js backend issues short-lived session tokens the client uses to start a video stream.

// app/api/heygen/token/route.ts
import { NextResponse } from "next/server";

export async function POST() {
  const res = await fetch("https://api.heygen.com/v1/streaming.create_token", {
    method: "POST",
    headers: {
      "x-api-key": process.env.HEYGEN_API_KEY!,
      "Content-Type": "application/json",
    },
  });

  if (!res.ok) {
    return NextResponse.json({ error: "Failed to create token" }, { status: 502 });
  }

  const { data } = await res.json();
  return NextResponse.json({ token: data.token });
}

Embedding the avatar on the client

The avatar component fetches a fresh token on mount, connects to Heygen's streaming service, and renders a <video> element. Keep this component lazy-loaded — the avatar bundle is not small, and most visitors will not trigger it.

"use client";
import { useEffect, useRef, useState } from "react";
import StreamingAvatar, { AvatarQuality, StreamingEvents } from "@heygen/streaming-avatar";

export function PropertyGuideAvatar() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [status, setStatus] = useState<"idle" | "ready" | "error">("idle");

  useEffect(() => {
    let avatar: StreamingAvatar | null = null;

    async function start() {
      const res = await fetch("/api/heygen/token", { method: "POST" });
      const { token } = await res.json();

      avatar = new StreamingAvatar({ token });

      avatar.on(StreamingEvents.STREAM_READY, (event) => {
        if (videoRef.current) {
          videoRef.current.srcObject = event.detail;
        }
        setStatus("ready");
      });

      await avatar.createStartAvatar({
        quality: AvatarQuality.Low, // mobile-first; upgrade on wifi
        avatarName: "property_guide_v1",
        knowledgeId: process.env.NEXT_PUBLIC_HEYGEN_KB!,
      });
    }

    start().catch(() => setStatus("error"));

    return () => {
      avatar?.stopAvatar();
    };
  }, []);

  return <video ref={videoRef} autoPlay playsInline muted={false} />;
}

Performance for 10K+ users

You do not hit 10K concurrent users by optimizing a single component. You hit it by cutting every slow decision from the critical path:

  • Ship the listings grid as a server component — first paint never waits on JavaScript
  • Cache listing queries in Redis with short TTLs; invalidate on mutation
  • Use MongoDB indexes aggressively — we had indexes on status, location, price range, and filter combinations
  • Lazy-load the Mapbox bundle; it is the single biggest thing on the map page
  • Lazy-load the Heygen avatar; most visitors never open it
  • Use next/image everywhere; WebP with proper sizes
  • Pre-render property detail pages with generateStaticParams for the top 1000 listings, SSR the tail
  • Cloudflare in front of everything; cache static assets and HTML where safe

What we would do differently

Two things, with the benefit of hindsight:

  1. Use Server Actions for dashboard mutations instead of a separate Express API. Next.js has matured to the point where the split just adds complexity.
  2. Start with Vapi or Retell AI for the voice layer if your use case needs phone calls, not just on-site chat. Heygen is fantastic for interactive video but it is not a phone agent.

The takeaway

A real estate marketplace in 2026 is three products in a trenchcoat: a performant listings site, a real-time backend, and an AI guide. Each one has its own pitfalls, and the glue between them is where most projects fall apart. The architecture above is what we now default to when a client asks us to build one — and it is flexible enough to adapt to any multi-sided marketplace, not just real estate.

If you are thinking about a marketplace build with real-time data and an AI assistant layer, we would love to talk through your specifics. That conversation is free and you will leave with a clearer picture of what it should cost and how long it should take.

Frequently asked questions

How much does a real estate marketplace like this cost to build?

A production marketplace with real-time updates, map search, and an AI avatar typically falls in a mid-five to low-six figure range depending on scope, integrations, and branding. A lean MVP without the AI layer can ship for significantly less. The biggest cost drivers are admin tooling, payment flows, and bespoke design.

Do I need Heygen specifically, or can I use a different AI avatar?

Heygen is one of the strongest options for interactive streaming avatars in 2026, but not the only one. D-ID and Synthesia also offer comparable APIs. The integration pattern in this post — server-issued session tokens plus lazy-loaded client component — applies to all of them.

Can I use Next.js Server Actions instead of a separate API?

For mutations inside the same Next.js app, yes — Server Actions remove an entire layer of code and typing. We still recommend a separate API process if you have heavy background work, long-running WebSocket connections, or integrations that benefit from running outside the Next.js request lifecycle.

How long does it take to build a marketplace like this?

A realistic timeline for a production-ready marketplace with the features described is 3 to 6 months with a small team. A quick MVP to validate with a handful of users can be assembled in 6 to 10 weeks. Trying to build everything at once is the single biggest scoping mistake we see.