Guide

Building a Multi-Tenant Real Estate SaaS Platform

How to design tenancy for a real estate SaaS: tenant hierarchy, data isolation with row-level security, custom domains, per-tenant MLS licenses and lifecycle.

Cover: three isolated tenant columns between tenant resolution at the edge and shared services.

The problem: the second brokerage

A real estate product built for one brokerage usually has that brokerage baked in: one logo, one MLS feed, one list of agents, one leads table. The engineering problem starts when a second customer signs. Now every query, cache key, background job, uploaded file and email has to answer a question it never had to answer before: whose is this? Adding a brokerage_id column to a few tables is the easy part. The rest is deciding where the tenant boundary runs through the edge, the application, the data, the MLS licenses and the invoice, and enforcing it somewhere a forgotten WHERE clause cannot defeat it.

This article is the architecture view. Whether to build a platform at all is covered in custom SaaS vs an off-the-shelf CRM, and the choice of database, search engine and cache in the tech stack guide.

What a tenant is in real estate SaaS

“Tenant” is not one thing in this industry, and the model has to say which level owns what before any table is designed. A workable hierarchy has three levels:

  • Brokerage (the account). The legal customer. It signs the contract, holds the MLS participation that listing data is licensed under, pays the invoice and, in most brokerages, owns the leads. This is the isolation boundary.
  • Team or office. A grouping inside a brokerage, used for lead routing, reporting and sometimes its own branding. It is a scope inside the boundary, not a second boundary.
  • Agent site. A website or subsite for one agent: its own domain, theme and lead-capture forms, but the brokerage's listing data and compliance rules.

Our recommendation is one hard boundary (the brokerage) and a tree of scopes inside it, because most awkward requirements are about movement within that tree. An agent leaves a team: which leads go with them? An agent moves to another brokerage on the same platform: nothing should move without an explicit export. A platform sold directly to solo agents collapses the tree to one level, but it is worth keeping the brokerage row anyway, because the MLS license still belongs to a broker.

The layers, and which are shared

Isolated tenant layers over shared services in a multi-tenant real estate platformRequests for three tenants arrive by subdomain, custom domain and path prefix. A shared edge terminates TLS and resolves each request to a tenant id. A shared, stateless application authenticates the user, checks membership in that tenant and sets the tenant context. Below it, each tenant has its own data partition for contacts, enquiries and saved searches, while one shared listing store is read through each tenant's MLS license filter. A per-tenant configuration store holds branding, domains, MLS licenses and plan limits and is read by the edge, the application and the license filter.Tenant Aarrives by subdomainTenant Barrives by custom domainTenant Carrives by path prefixEdge: TLS and tenant resolutionhost name or path → tenant id · unknown host → not found · shared by all tenantsrequest + tenant idApplication (shared, stateless)authenticate → check membership in this tenant → set tenant context for the transactionPer-tenant data partitionsevery row carries a tenant id; the database enforces itTenant Acontactsenquiriessaved searchesTenant Bcontactsenquiriessaved searchesTenant Ccontactsenquiriessaved searchesShared listing storeone copy per MLS sourceLicense filtertenant → licensed sourcesa tenant reads only thesources it holds a license forPer-tenant configurationdomains · theme and branding · MLS licenses · plan limits · feature flagsread by the edge (domains), the application (theme, limits) and the license filter (dashed arrows)
Illustrative layering: the edge and application are shared; operational data is partitioned per tenant; listing data is stored once and read through each tenant's license filter.

The same picture as a list:

  1. Edge. Shared. Terminates TLS and turns a host name or path into a tenant id; an unknown host gets a not-found response, never a default tenant.
  2. Application. Shared and stateless. Authenticates the user, checks that the user belongs to the resolved tenant, and sets the tenant context for the database transaction.
  3. Per-tenant data partitions. Contacts, enquiries, saved searches and other operational records, each row carrying a tenant id that the database enforces.
  4. Shared listing store. One copy of listing data per MLS source, read through a filter that maps each tenant to the sources it is licensed for.
  5. Per-tenant configuration. Domains, theme, MLS licenses, plan limits and feature flags, read by the edge, the application and the license filter.

Three tenancy models

The data layer has three standard shapes. They differ in how isolation is enforced, what a schema change costs, and what happens when one tenant is very large or asks for its own environment.

QuestionShared schema, tenant keySchema per tenantDatabase per tenant
What isolates tenantsA tenant id on every row, enforced by the application and, better, by row-level securityNamespaces: PostgreSQL lists allowing many users to use one database without interfering with each other as a reason to use schemasSeparate databases and, if wanted, separate credentials and hosts
Schema migrationOnceOnce per tenant, so migrations need orchestration and a record of which tenant is on which versionOnce per tenant, plus per-database connection handling
ConnectionsOne poolOne pool; the search path or qualified names select the tenantA pool per database, or a router in front
Queries across tenants (platform reporting, support tools)Ordinary SQL under a privileged roleA union across schemasAn export to a separate reporting store
One tenant's backup, restore or moveHardest: a filtered export and importPer-schema dumpSimplest: the database is the unit
Large tenant countFits naturallyCatalog size and migration time grow with every tenantOperational cost grows with every tenant
When we would pick itThe default for many similar tenants: agents, teams, small and mid-size brokeragesRarely as a first choice; a middle step for a modest, stable number of tenantsA few large customers with contractual isolation, data-residency or custom-schema demands

MongoDB's own guidance lines up with this. It suggests one database per tenant for a small and stable number of tenants (up to a few hundred) with varied data requirements and strict access control requirements, and shared collections with a tenantId field in each document when the number of tenants keeps growing and their data looks alike. It also names the cost of the shared model plainly: the multi-tenancy logic exists in the application layer, and every database query must filter by that field. The third option, a collection per tenant in one database, is one it tells readers to avoid.

The models combine. A sound default is a shared schema for everyone, with the tenant id designed so that a single large brokerage can later be moved to its own database without changing application code: the application always asks a router for “the connection for tenant X”, even while the router returns the same pool for all of them.

Enforcing isolation in the database

In the shared-schema model, the weakest design is one where every query author must remember the tenant filter. PostgreSQL can enforce it instead. With row-level security enabled on a table, all normal access to the table for selecting rows or modifying rows must be allowed by a row security policy, and if no policy exists, a default-deny policy is used, meaning that no rows are visible or can be modified. A missing policy fails closed, which is the property you want.

Illustrative schema (names and columns are examples, not a client system):

-- Illustrative. The tenant boundary is the brokerage (account).
CREATE TABLE tenant (
  id          uuid PRIMARY KEY,
  name        text NOT NULL,
  status      text NOT NULL DEFAULT 'active',   -- active | suspended | deleting
  plan        text NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE tenant_domain (
  host        text PRIMARY KEY,                 -- 'oak.example-platform.com' or 'www.oakrealty.com'
  tenant_id   uuid NOT NULL REFERENCES tenant(id),
  site_id     uuid,                             -- which agent or team site, if any
  verified_at timestamptz,                      -- ownership proven before it is served
  tls_status  text NOT NULL DEFAULT 'pending'
);

CREATE TABLE tenant_mls_license (
  tenant_id     uuid NOT NULL REFERENCES tenant(id),
  mls_source    text NOT NULL,                  -- originating system
  license_type  text NOT NULL,                  -- e.g. IDX, VOW, back office
  approved_at   timestamptz,
  PRIMARY KEY (tenant_id, mls_source, license_type)
);

CREATE TABLE contact (
  tenant_id   uuid NOT NULL REFERENCES tenant(id),
  id          uuid NOT NULL,
  team_id     uuid,                             -- scope inside the tenant
  owner_agent uuid,
  email       text,
  created_at  timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (tenant_id, id),                  -- tenant first: every key and index starts with it
  UNIQUE (tenant_id, email)                     -- uniqueness is per tenant, never global
);

Illustrative policy:

-- Illustrative. One policy per tenant-owned table.
ALTER TABLE contact ENABLE ROW LEVEL SECURITY;
ALTER TABLE contact FORCE ROW LEVEL SECURITY;    -- the owner is subject to it too

CREATE POLICY tenant_isolation ON contact
  USING      (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

-- Per request, inside the transaction, with a parameter (never string-built SQL):
BEGIN;
SELECT set_config('app.tenant_id', $1, true);    -- true = local to this transaction
SELECT id, email FROM contact;                   -- returns this tenant's rows only
COMMIT;

Five details in that example come straight from the documentation, and each one is a way to get it wrong:

If the tenant-owned tables grow large, the same tenant id is a natural partition key. PostgreSQL notes that dropping an individual partition is far faster than a bulk operation and avoids the vacuum overhead of a bulk delete, which is useful at offboarding; it also warns that too many partitions can mean longer query planning times and higher memory consumption, so a partition per tenant suits a bounded number of large tenants, not an open-ended count of agent sites.

The database is only one store. The same tenant id has to prefix every cache key, sit in every search query as a filter the caller cannot remove, and form the first path segment of every object-storage key. A cache entry keyed by URL path alone is the classic leak: two agent sites share the path /listings and one receives the other's page.

Tenant resolution and custom-domain TLS

There are three ways a request names its tenant, and most platforms end up supporting two:

  • Subdomain (for example oak.example-platform.com). One wildcard DNS record and one wildcard certificate cover every tenant. Nothing to provision per signup.
  • Custom domain (for example www.oakrealty.com). What agents and brokerages actually want on a business card. It needs a domain table, proof of ownership, a certificate per domain and renewal.
  • Path prefix (for example /t/oak/...). Simple for an internal dashboard; a poor fit for public sites because cookies, robots rules and search-engine identity are shared across tenants.

Resolve the tenant once, at the edge, from a lookup table, and pass the id inward as trusted context. Never take the tenant id from a query parameter, a request body or an unvalidated header: that turns tenant selection into something the caller controls.

Custom-domain TLS is a system to plan, not a checkbox. If certificates come from Let's Encrypt, the published limits shape the design: up to 300 new orders can be created by a single account every 3 hours, and up to 50 certificates can be issued per registered domain every 7 days. The second limit bites when you issue a certificate per subdomain of your own platform domain instead of one wildcard, and wildcards have their own condition: the HTTP-01 challenge cannot be used to issue wildcard certificates, while DNS-01 allows you to issue wildcard certificates. A workable plan is a DNS-01 wildcard for platform subdomains and on-demand HTTP-01 certificates for customer domains, issued only for hosts already present and verified in the domain table, with a queue for issuance and an alert on renewals that fail.

Authentication and authorization boundaries

Two questions are easy to merge and must stay separate: who is this user, and which tenant is this request for.

  • Membership, not a column on the user. A person can belong to more than one tenant (an agent who changes brokerage, a transaction coordinator who serves two). Model a membership row per user and tenant, with the role on the membership.
  • Sessions are scoped to a tenant. A session issued on one tenant's domain is not valid on another's. Host-only cookies on custom domains give this for free; on shared subdomains, set the cookie for the exact host, not the parent domain.
  • Check order. Tenant from the host, user from the session, membership of that user in that tenant, then the role check. Fail at the first missing link with the same not-found response, so the reply does not reveal that a record exists in another tenant.
  • Platform staff are a separate path. Support access to a tenant goes through an explicit, time-limited, logged impersonation flow under its own role, not a flag that bypasses checks.

Role design inside a tenant (record visibility, field-level restrictions, audit trails) is covered in the investor portal security checklist.

Per-tenant configuration and white-label branding

White-label means the tenant's visitors never see the platform. The discipline is to keep that as data, not code: design tokens, navigation and page content, lead-form settings, email sender identity, feature flags and plan limits, stored per tenant and per site. A tenant that needs a custom template gets it through the same theme interface, not a fork of the application.

Three engineering points follow:

  • Configuration is read on every request, so it is cached per tenant and invalidated by a version number on the tenant row, not by a timer.
  • Email identity is per tenant. Sending from a tenant's own domain depends on DNS records that tenant has to publish, so onboarding needs a verification step; until it passes, mail goes from a platform address that names the brokerage.
  • Tenant-supplied content is untrusted input. Custom scripts, HTML blocks and uploaded logos are served from the tenant's own origin and sanitized, so one site's embed cannot run against another site's session.

Our real estate SaaS case study describes this problem from the failing side: a white-label agent website platform where all agents shared configuration, leads and property data in flat collections, so that one agent's misconfiguration could affect every other agent's website. The page reports that the MongoDB collections were restructured for multi-tenant separation and white-label agent themes deployed, and that the platform grew from 5 agents to more than 200.

The tenant boundary is also a data-license boundary

This is the part general SaaS guides leave out. Listing data is not the platform's to share out as it likes: each brokerage displays it under its own MLS participation. MLS Grid's public IDX Rules, used here as one published example, say that any IDX display must be under the actual and apparent control of a single Member Participant who has executed a MLS GRID Data License Agreement, and count as evidence of apparent control that the domain name and branding on the website distinguish the Member Participant from nonparticipating firms. Access itself is granted per subscription: MLS Grid issues an API token once a subscription has been created, a licensee added, and the MLS has approved.

What that means for the architecture, as recommendations:

  • Record licenses as tenant data. The tenant_mls_license table above is what the listing filter reads. A tenant with no approved license for a source sees no listings from it, whatever the platform holds.
  • Store listings once, authorize per tenant. Whether one vendor feed may serve many brokerages' sites, or each brokerage needs its own credentials and feed, is set by each MLS and platform. Ask that question in writing for every MLS before choosing between a shared listing store and per-tenant feeds.
  • Display rules are per source and per site. Attribution, brokerage identification and seller opt-outs apply to each tenant's display, so the display-policy filter in the MLS data pipeline takes the tenant as an input.
  • Offboarding ends the license. When a brokerage leaves, or its MLS approval lapses, its sites stop showing that source the same day. Make this a status on the license row, checked on read.

Rules differ between MLSs and change; the guide to MLS data access covers who may license what.

Noisy neighbors, limits and background work

In a shared system one tenant's activity is every tenant's latency. The typical causes in this domain are a bulk contact import, a large email campaign, an agent site being crawled hard, and a report that scans years of activity.

  • Rate limits keyed by tenant at the edge and the API, with separate budgets for public site traffic and authenticated API use, so a crawler on one site cannot exhaust that brokerage's CRM, or anyone else's.
  • Quotas from plan limits: sites, seats, contacts, storage, monthly sends. Enforced where the resource is created, and visible to the tenant before they hit them.
  • Database guardrails: statement timeouts on the application role, tenant-leading indexes so one tenant's query never scans another's rows, and heavy reports sent to a replica or an asynchronous export.
  • Fair background work. Every job carries the tenant id and re-establishes tenant context before touching data. A single first-in, first-out queue lets one tenant's 50,000-row import (an illustrative figure) delay everyone's lead notifications, so we would separate queues by urgency and cap concurrency per tenant within each. The mechanics are in reliable background jobs with Redis and BullMQ.

Billing boundaries

Billing is a consumer of tenancy data, not its source: the platform decides what a tenant has and has used, and the billing system turns that into invoices. A feature check should never need to call the payment provider.

  • Usage is an append-only record per tenant, written by the system that did the work, with an idempotency key so a retried job is not counted twice.
  • Entitlements live in the platform. The tenant row holds the plan and limits; billing events update them through a webhook handler built as described in reliable webhook integrations.
  • Suspension is a tenant state, with defined behavior: public sites keep serving or not, logins read-only or blocked, data retained for how long. Decide it before the first failed payment.
  • The hierarchy shows up on the invoice. A brokerage paying for its agents, and an agent paying for their own site under a brokerage, are different billing parents for the same site row.

Tenant lifecycle: provisioning, export, deletion

Provisioning should be one idempotent workflow that can be re-run after a failure at any step: create the tenant row, default configuration and first admin membership; reserve the subdomain; record requested MLS sources as pending; create the billing customer; then mark the tenant active. Custom domains and MLS approval finish later, on their own clocks, and the tenant can see their status.

Export is a feature, not a support ticket. A brokerage will ask for its contacts, enquiry history and site content when it leaves, and sometimes when one agent leaves. Because every row carries the tenant id, an export is a walk over the tenant-owned tables. Listing data is not part of it: that belongs to the MLS license, not the tenant.

Deletion has to reach every store the tenant id reached: primary database, search index, caches, object storage, queued jobs, analytics, and backups according to a stated retention period. We would run it as a staged job (suspend, export offered, soft-delete with a grace period, hard-delete, certificate and domain release) and keep a deletion record that holds no personal data. What must be retained, and for how long, is a question for the customer's counsel and your contract, not something to assume.

A pre-launch isolation checklist

  • Every tenant-owned table has a tenant id, a tenant-leading primary key and a row-level policy; a test fails the build if a new table lacks one.
  • The application's database role owns no tables and has no bypass attribute.
  • An automated test signs in as tenant A and requests tenant B's record ids through every API route; the expected result is not-found every time.
  • Cache keys, search filters and storage paths include the tenant id; a test requests the same path on two tenant hosts and compares the responses.
  • Background jobs fail if they are enqueued without a tenant id.
  • An unknown host returns not-found; a custom domain is served only after verification.
  • A tenant with no approved license for an MLS source gets zero listings from it.
  • Export and deletion have been run end to end on a test tenant, including the search index and object storage.

Frequently Asked Questions

Which tenancy model should a real estate SaaS start with?

For many similar tenants, a shared schema with a tenant id on every row, enforced by the database, is a sound default. MongoDB’s guidance points the same way: shared collections with a tenantId field in each document when tenant numbers keep growing, and one database per tenant for a small and stable number of tenants with strict access control requirements. Design the connection layer so a large tenant can be moved to its own database later.

Is row-level security enough to isolate tenants in PostgreSQL?

It is a strong second line, not the only one. With it enabled and no policy, a default-deny policy is used, but superusers and roles with the BYPASSRLS attribute always bypass the row security system, and caches, search indexes and file storage sit outside the database. Keep the application filter, connect as a role that owns no tables, and test cross-tenant access through the API.

Can several brokerages share one MLS feed on a multi-tenant platform?

That is decided by each MLS, not by the platform. As one published example, MLS Grid’s IDX Rules require that any IDX display be under the actual and apparent control of a single Member Participant who has executed a data license agreement. Record each tenant’s approved licenses and filter listings by them, and ask each MLS in writing whether a shared vendor feed is permitted.

Documentation and rules were read from the linked PostgreSQL, MongoDB, PgBouncer, Let's Encrypt and MLS Grid pages on October 12, 2026. MLS Grid's IDX Rules are quoted as one public example; every MLS sets its own terms, and your license agreement decides. Schemas, policies, host names and figures marked illustrative are examples, not measurements or descriptions of a client system.

Designing a multi-tenant platform, or untangling a single-tenant one? See our SaaS development service, and the companion article on real estate CRM architecture for the lead data that lives inside each tenant. Every project starts with a 30-minute technical discovery call with an engineer.

About the examples. Every code sample here is illustrative. The schema and the row-level security policy were run in PostgreSQL 16 on October 13, 2026 as a non-superuser role: each tenant read only its own rows, a cross-tenant insert was refused, and a query with no tenant set failed.

Technical consultation

Designing a Multi-Tenant Real Estate Platform?

Tell us who your tenants are (brokerages, teams or agent sites), which MLSs they license and how the data is separated today. An engineer will walk through the tenancy model and isolation it needs.

  • A 30-minute technical discovery call with an engineer, not a salesperson.
  • Fixed-scope projects, monthly retainers, or a combination, agreed after the call.
  • Small, well-defined tasks get a scoped estimate. We reply within 1 business day.

Prefer to pick a time? Book a call on Calendly

Tell us about your project

We'll review your details and reach out within 1 business day.

Book Strategy CallCase Studies