Multi-Tenant AI Calling Infrastructure — Deploying Across 10 Real Estate Projects Simultaneously
A complete architecture guide for multi-tenant AI Calling infrastructure serving multiple real estate projects — what to share vs. isolate per tenant, the TenantConfig data model, tenant identification and call routing, per-tenant system prompt assembly, four-layer data isolation, multi-tenant monitoring and concurrency quotas, group vs. project-level operational controls, and a phased rollout plan across 10 projects.
⏱ 14 min read🏢 Technical Architecture & Developer Guides📅 6 July 2026
10 Projects Should Run on One Infrastructure, Not Ten
A real estate developer group operating 10 active projects simultaneously — three in Gurgaon, two in Noida, two in Pune, one in Hyderabad, one in Mumbai, and one pre-launch in Bangalore — does not have 10 separate AI Calling deployments. It has one AI Calling infrastructure serving 10 tenants, each with its own project knowledge base, calling script, qualification criteria, CRM configuration, telephony number pool, and performance dashboard.
The distinction between a multi-project deployment and a multi-tenant architecture is not semantic. In a multi-project deployment, 10 separate instances of the AI system run independently, and operational overhead grows linearly with project count. A multi-tenant architecture is a single, shared infrastructure that logically isolates each project's configuration, data, and performance metrics — while sharing the underlying compute, telephony, and LLM inference layers that make shared deployment cost-effective.
What Needs to Be Shared, What Must Be Isolated
The first design decision in multi-tenant AI Calling is which components are shared across tenants and which are logically isolated per tenant.
Shared Infrastructure (Compute Pool)
LLM inference — a single LLM API endpoint serves all tenants; tenant-specific context is injected in the system prompt while the model itself is shared, eliminating redundant per-project LLM integration cost
ASR engine — the ASR pipeline is shared, with audio streams from all active calls passing through the same layer, differentiated only by session metadata carrying tenant context
TTS engine — the synthesizer is shared, but voice selection is per-tenant, so a luxury project can use a cloned voice while an affordable project uses a standard Hinglish voice on the same engine
Call orchestration service — a single horizontally-scaled orchestrator manages call lifecycle for all tenants, remaining stateless with call state living in Redis keyed by call_id and tenant_id
Monitoring and alerting — a single metrics stack monitors all tenants, with dashboards as tenant-scoped views into the shared metrics store
Isolated Per Tenant (Logical Isolation)
Project knowledge base — each tenant's project facts (pricing, inventory, RERA registration, possession dates, amenities, site visit slots) are stored in a tenant-scoped knowledge store
Qualification criteria — each project defines its own disqualification rules; one project may accept ₹45L–₹75L buyers while another's minimum is ₹2.5Cr
Lead database — leads are stored in tenant-isolated partitions with cross-tenant access blocked at the data layer
Telephony number pool — each tenant operates from a dedicated virtual number pool so a shared pool's high volume from one project cannot trigger spam flags on another's numbers
CRM configuration — each project may integrate with different CRM instances or different campaign IDs within the same CRM
Performance dashboards — project marketing heads see only their project's metrics, not commercially sensitive group-wide lead counts and conversion rates across competing projects
The Tenant Configuration System
Every tenant in the system is represented by a TenantConfig object that defines all project-specific parameters:
@dataclass
class TenantConfig:
# Identity
tenant_id: str # "PROJ-GGM-001"
tenant_name: str # "Sector 65 Golf Course Extension"
developer_group_id: str # "DEVGROUP-ACME"
# Project knowledge base
project_name: str
project_location: str
rera_number: str
harera_number: Optional[str]
# Pricing and inventory
price_floor_inr: int
price_ceiling_inr: int
available_bhk_configs: list # [3, 4]
carpet_area_by_bhk: dict # {3: 1050, 4: 1450}
sba_by_bhk: dict # {3: 1680, 4: 2320}
possession_date: str
possession_committed_rera: bool
# Calling configuration
calling_script_template: str
qualification_objectives: list
disqualification_rules: dict
objection_handling_map: dict
site_visit_slot_options: list
# Language and voice
primary_language: str # "hinglish"
tts_voice_config: dict
# Telephony
number_pool: list
telephony_provider: str # "exotel"
telephony_api_credentials: str # Reference to secrets manager
# CRM integration
crm_provider: str # "sell.do"
crm_campaign_id: str
crm_webhook_url: str
crm_field_mapping: dict
# Human fallback
escalation_queue: str
fallback_agent_extensions: list
# Performance thresholds
max_call_duration_seconds: int
transfer_rate_alert_threshold: float
💡
Tenant configs are stored with version history — every change creates a new version record, enabling rollback if a script change degrades conversion rate. The config is loaded into a Redis cache on startup and refreshed on update events.
The Call Router: Tenant Identification and Context Loading
Every inbound webhook from the telephony layer or outbound call event carries a phone number or campaign ID that the router uses to identify the tenant:
class MultiTenantCallRouter:
def __init__(self, config_store: TenantConfigStore, redis: Redis):
self.config_store = config_store
self.redis = redis
async def route_incoming_call(self, call_event: dict) -> tuple[TenantConfig, CallSession]:
tenant_id = await self._identify_tenant(call_event)
if not tenant_id:
await self._handle_unknown_caller(call_event)
return None, None
tenant_config = await self.config_store.get(tenant_id)
lead_data = await self._load_lead_data(
tenant_id=tenant_id,
phone_number=call_event.get("from_number")
)
session = CallSession(
call_id=call_event["call_id"],
tenant_id=tenant_id,
tenant_config=tenant_config,
lead_data=lead_data,
call_start_time=datetime.utcnow()
)
await self.redis.setex(
f"session:{tenant_id}:{session.call_id}",
ttl=7200,
value=session.to_json()
)
return tenant_config, session
async def _identify_tenant(self, call_event: dict) -> Optional[str]:
# Method 1: Match outbound caller ID to tenant number pool
from_number = call_event.get("to_number")
tenant = await self.config_store.find_by_number(from_number)
if tenant:
return tenant.tenant_id
# Method 2: Campaign ID in event metadata
campaign_id = call_event.get("campaign_id")
if campaign_id:
return await self.config_store.find_by_campaign(campaign_id)
return None
Prompt Assembly: Per-Tenant System Prompt Construction
The LLM system prompt is assembled dynamically from the tenant config on every inference call — there is no static system prompt in a multi-tenant system:
def assemble_system_prompt(
tenant_config: TenantConfig,
session: CallSession,
state_context: str
) -> str:
prompt = f"""ROLE:
You are a professional outbound qualification specialist for {tenant_config.developer_group_id},
calling about {tenant_config.project_name} in {tenant_config.project_location}.
Your name is Priya.
YOUR OBJECTIVES (in priority order):
{chr(10).join(f"{i+1}. {obj}" for i, obj in enumerate(tenant_config.qualification_objectives))}
PROJECT FACTS (use ONLY these — never invent):
- Project: {tenant_config.project_name}
- Location: {tenant_config.project_location}
- RERA: {tenant_config.rera_number or tenant_config.harera_number}
- Available: {_format_pricing(tenant_config)}
- Possession: {tenant_config.possession_date} (RERA committed: {tenant_config.possession_committed_rera})
- Site visit slots: {", ".join(tenant_config.site_visit_slot_options)}
DISQUALIFICATION RULES:
- Budget below floor after 2 attempts → trigger transfer
- BHK not in {tenant_config.available_bhk_configs} → attempt alternate or transfer
LANGUAGE: {tenant_config.primary_language.upper()} — mirror buyer's language mix exactly.
RESPONSE RULES:
- Maximum 35 words per response
- One question per turn only
- If question outside PROJECT FACTS → offer WhatsApp + site visit
{state_context}
"""
return prompt
Data Isolation: Preventing Cross-Tenant Leakage
Data isolation is enforced at four layers.
Redis key namespacing — all session data is keyed as session:{tenant_id}:{call_id}; any Redis read that does not match the session's tenant_id prefix raises an isolation violation error
Database row-level security — the leads and calls tables include a tenant_id column, and Row Level Security policies enforce that every query returns only rows matching the authenticated tenant context
CRM write segregation — CRM write operations are constructed from the tenant config's campaign ID and field mapping, making it structurally impossible for a call from one tenant to write to another tenant's CRM campaign
Telephony isolation — the telephony client for each call is initialized with the tenant's own API credentials from the secrets manager, not a shared credential, architecturally blocking cross-tenant telephony actions
Multi-Tenant Performance Monitoring
With 10 projects running simultaneously, a single performance degradation may affect all tenants or only a subset. The monitoring system must distinguish tenant-specific problems from system-wide problems.
Metric
System-Wide Alert
Tenant-Specific Alert
LLM inference P95 latency
> 800ms across all tenants
> 800ms for single tenant only
ASR WER spike
> 15% across all active calls
> 15% for calls in specific city/market
Transfer rate
> 20% system-wide
> 20% for single project
Call drop rate
> 5% system-wide
> 5% for specific telephony number pool
CRM sync failure
> 1% system-wide
> 1% for specific CRM integration
Concurrent call ceiling
> 80% of provisioned capacity
Single tenant consuming > 40% of pool
Each tenant is assigned a maximum concurrent call quota — typically proportional to monthly lead volume — so a tenant running a weekend campaign launch cannot starve the infrastructure of other tenants:
TENANT_CONCURRENCY_LIMITS = {
"PROJ-GGM-001": 50, # Large project, high volume
"PROJ-GGM-002": 30, # Medium project
"PROJ-NOI-001": 40,
"PROJ-PUN-001": 25,
# ... etc.
}
async def can_initiate_call(tenant_id: str) -> bool:
current = await redis.get(f"concurrency:{tenant_id}")
limit = TENANT_CONCURRENCY_LIMITS.get(tenant_id, 20)
return int(current or 0) < limit
Operational Controls: Group-Level and Project-Level
A multi-tenant system requires two tiers of operational controls. Group-level controls — managed by the developer group's technical or marketing head — include pausing all calling system-wide, viewing the combined performance dashboard, adjusting the global concurrent call ceiling, and deploying system-wide prompt updates for language or compliance rules. Project-level controls — managed by individual project marketing managers — include pausing or resuming calling for their specific project only, updating site visit slot availability, adjusting calling hours, reviewing their own performance dashboard, and exporting call transcripts for quality review.
This two-tier control model prevents project managers from accidentally affecting other projects while giving them full autonomy over their own deployment.
Deployment Pattern: Rolling Out to 10 Projects
Migrating 10 projects from human calling to multi-tenant AI Calling is a sequenced deployment, not a simultaneous switch.
Week 1–2 — deploy infrastructure; onboard 2 pilot projects, one high-volume and one medium-volume, to establish baseline metrics
Week 3–4 — review pilot data; tune prompts, transfer triggers, and TTS voice for the 2 pilot markets to achieve target qualification rate
Week 5–6 — onboard 4 additional projects using learnings from the pilots; shared infrastructure scales horizontally with no new compute provisioning required if within capacity
Week 7–8 — onboard the remaining 4 projects for full 10-project operation; establish group-level reporting cadence
Month 3+ — optimize per-tenant; individual projects below target conversion get prompt revisions, trigger threshold adjustments, or A/B tested opening scripts, all within the same shared infrastructure
Frequently Asked Questions
Yes, and this needs explicit deduplication logic at the group level to prevent the same buyer from receiving calls from two different projects within a short window. Implement a group-level deduplication table keyed by phone number with a cooldown TTL: if a buyer received a call from Project A at 10:00 AM, Project B cannot initiate a call to the same number until a minimum interval — typically 4–6 hours — has elapsed. This prevents the buyer experience of being called twice by the "same developer," which damages trust and increases complaint rate.
A new tenant onboarding in a production multi-tenant system follows a blue/green deployment pattern for the config: create the new TenantConfig record in the config store in an inactive state so the system loads it but routes no calls to it; test the new tenant configuration with 5–10 internal test calls against a test lead list, verifying knowledge base accuracy, transfer logic, and CRM write-back; then activate the tenant by flipping the status field from inactive to active, and the router begins routing calls to the new tenant from the next outbound batch. The existing 9 tenants experience zero disruption — the new tenant's activation is a config state change, not an infrastructure change.
Build a no-code configuration portal where project marketing managers can update site visit slot availability, pricing range, possession date updates, and pre-defined objection handling responses — all scoped to their project only. Changes save to the tenant config version history and propagate to the Redis cache within 60 seconds without a deployment. Changes that affect the LLM system prompt structure, such as adding new qualification objectives or new objection patterns, should require a technical review step before activation — a "pending review" state that a technical administrator approves. This prevents a non-technical project manager from inadvertently breaking the qualification logic with a poorly structured prompt addition.
Set the initial quota proportional to each project's expected monthly lead volume divided by a target daily calling window, then adjust based on observed peak-hour demand over the first 2–4 weeks of live data. A project generating 3,000 leads/month needs a meaningfully larger concurrent quota than one generating 300 leads/month, but quotas shouldn't be purely linear — smaller projects still need enough headroom to handle their own campaign-driven bursts (a weekend launch push) without being starved by a larger project's steady-state volume. Review and rebalance quotas monthly as project lead volumes shift over the sales cycle, particularly around launch and pre-launch phases where volume can spike 5–10× temporarily.
Yes, without capacity management this is the primary risk of shared infrastructure — which is exactly why per-tenant concurrency quotas exist at the orchestration layer, independent of the underlying LLM/ASR/TTS provider's own rate limits. Configure the shared pool's total capacity with meaningful headroom above the sum of all tenant quotas (typically 20–30% buffer), and monitor system-wide latency percentiles separately from tenant-specific ones so a spike affecting all tenants is distinguished quickly from a spike isolated to one tenant's traffic pattern. Providers with hard account-level rate limits (like some LLM APIs) may require multiple API keys or provider accounts once combined tenant volume approaches those limits.
Disclaimer: Multi-tenant architecture patterns, concurrency limits, data isolation approaches, and operational controls described in this article reflect production deployment practices as of Q2 2026. Actual infrastructure requirements, scaling thresholds, and resource quotas depend on call volume, LLM inference latency characteristics, and telephony provider capacity. Code samples are illustrative and require adaptation to specific technology stacks, cloud providers, and organizational deployment requirements. Data isolation designs should be reviewed by a qualified security engineer before production deployment.