DEV Community

ZNY
ZNY

Posted on

DEV.TO ARTICLE 48: Building Multi-Tenant SaaS with AI: Architecture Patterns for 2026

Target Keyword: "multi-tenant saas architecture ai"
Tags: saas,architecture,ai,programming,developer
Type: Guide


Content

Building Multi-Tenant SaaS with AI: Architecture Patterns for 2026

Building a SaaS product that serves multiple customers (multi-tenancy) while integrating AI capabilities requires careful architecture. Here's the complete guide to building a scalable, secure multi-tenant AI SaaS.

What Is Multi-Tenancy?

In a multi-tenant system, one application instance serves multiple customers (tenants), each with their own data and settings. This is how products like Salesforce, HubSpot, and Notion work.

Benefits:

  • Cost efficiency (shared infrastructure)
  • Easier maintenance (one codebase)
  • Fast onboarding (new tenant = new row in database)

Database Architecture

# Option 1: Shared database, shared schema (most common)
class TenantAwareModel:
    """All models inherit from this for tenant isolation."""
    tenant_id: int

    @classmethod
    def get_for_tenant(cls, tenant_id, **filters):
        return cls.query.filter_by(tenant_id=tenant_id, **filters)

class User(TenantAwareModel, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    tenant_id = db.Column(db.Integer, db.ForeignKey('tenants.id'), nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False)
    # ...

# Always filter by tenant_id
def get_users(tenant_id):
    return User.query.filter_by(tenant_id=tenant_id).all()
Enter fullscreen mode Exit fullscreen mode

Tenant Isolation Middleware

from flask import g, request

@app.before_request
def before_request():
    """Extract and validate tenant from request."""
    # Get tenant from subdomain, header, or JWT
    host = request.host

    if host.startswith('app.'):
        subdomain = host.split('.')[0]
        tenant = Tenant.query.filter_by(slug=subdomain).first()
    else:
        # API calls use header
        tenant_id = request.headers.get('X-Tenant-ID')
        tenant = Tenant.query.get(tenant_id)

    if not tenant:
        abort(403, "Invalid tenant")

    g.tenant = tenant
    g.tenant_id = tenant.id
Enter fullscreen mode Exit fullscreen mode

AI Service Per Tenant

class TenantAIClient:
    """AI client configured per tenant."""

    def __init__(self, tenant_id: int):
        self.tenant_id = tenant_id
        self.tenant = Tenant.query.get(tenant_id)
        self.config = self.tenant.ai_config

    def chat(self, messages: list[dict]) -> str:
        """Call AI with tenant-specific settings."""
        # Apply tenant-specific system prompt modifications
        enhanced_messages = self._apply_tenant_context(messages)

        # Use tenant's own API key or shared pool
        api_key = self.tenant.ai_api_key or AI_POOL.get_key()

        return call_ai(api_key, enhanced_messages)

    def _apply_tenant_context(self, messages):
        """Inject tenant-specific context into prompts."""
        if not self.config.get('inject_context'):
            return messages

        context = f"""
Tenant: {self.tenant.name}
Plan: {self.tenant.plan}
Custom Instructions: {self.tenant.ai_instructions or 'None'}
"""
        # Prepend to first user message
        messages = list(messages)
        if messages and messages[0]['role'] == 'user':
            messages[0]['content'] = context + "\n\n" + messages[0]['content']
        return messages
Enter fullscreen mode Exit fullscreen mode

Tenant-Specific AI Features

class TenantFeatureFlags:
    FEATURES = {
        'basic_chat': {'tier': 'free', 'limit': 100},
        'code_review': {'tier': 'pro', 'limit': 1000},
        'document_analysis': {'tier': 'pro', 'limit': 500},
        'custom_prompts': {'tier': 'enterprise', 'limit': None},
    }

    @classmethod
    def is_enabled(cls, tenant, feature: str) -> bool:
        tenant_tier = TIER_ORDER.index(tenant.plan)
        feature_tier = TIER_ORDER.index(cls.FEATURES[feature]['tier'])
        return tenant_tier >= feature_tier

    @classmethod
    def check_limit(cls, tenant, feature: str) -> bool:
        limit = cls.FEATURES[feature]['limit']
        if limit is None:
            return True

        usage = UsageLog.query.filter_by(
            tenant_id=tenant.id,
            feature=feature
        ).count()

        return usage < limit
Enter fullscreen mode Exit fullscreen mode

Usage Tracking and Billing

class UsageTracker:
    def track(self, tenant_id: int, feature: str, tokens_used: int, cost_usd: float):
        log = UsageLog(
            tenant_id=tenant_id,
            feature=feature,
            tokens_used=tokens_used,
            cost_usd=cost_usd,
            timestamp=datetime.utcnow()
        )
        db.session.add(log)

        # Update tenant's running total
        tenant = Tenant.query.get(tenant_id)
        tenant.current_period_cost += cost_usd

        # Check if over budget
        if tenant.monthly_budget and tenant.current_period_cost > tenant.monthly_budget:
            self._notify_over_budget(tenant)

        db.session.commit()

    def get_tenant_usage(self, tenant_id: int, period: str = 'month') -> dict:
        start = self._get_period_start(period)
        logs = UsageLog.query.filter(
            UsageLog.tenant_id == tenant_id,
            UsageLog.timestamp >= start
        ).all()

        return {
            'total_cost': sum(log.cost_usd for log in logs),
            'total_tokens': sum(log.tokens_used for log in logs),
            'by_feature': self._group_by_feature(logs)
        }
Enter fullscreen mode Exit fullscreen mode

Onboarding New Tenants

@app.route('/api/tenants', methods=['POST'])
def create_tenant():
    data = request.get_json()

    # Create tenant record
    tenant = Tenant(
        name=data['company_name'],
        slug=data['slug'],
        plan='free',
        owner_email=data['email']
    )
    db.session.add(tenant)
    db.session.flush()  # Get tenant.id

    # Create owner user
    user = User(
        tenant_id=tenant.id,
        email=data['email'],
        role='owner',
        hashed_password=hash_password(data['password'])
    )
    db.session.add(user)

    # Set up default AI settings
    ai_config = AIConfig(
        tenant_id=tenant.id,
        model='claude-3-5-sonnet-20241022',
        max_tokens=1024,
        temperature=0.7
    )
    db.session.add(ai_config)

    db.session.commit()

    # Send welcome email
    send_welcome_email(tenant, user)

    return {"tenant_id": tenant.id, "slug": tenant.slug}, 201
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

# 1. Always validate tenant ownership
def get_tenant_resource(resource_id, tenant_id):
    resource = Resource.query.get(resource_id)
    if resource.tenant_id != tenant_id:
        abort(403, "Access denied")
    return resource

# 2. Never rely solely on URL params for tenant ID
# ✅ Good: Use g.tenant_id from middleware
# ❌ Bad: tenant_id = request.args.get('tenant_id')

# 3. Sanitize AI prompts to prevent cross-tenant data leakage
def sanitize_tenant_prompt(tenant_id, user_input):
    # Block attempts to extract other tenants' data
    blocked = ['tenant_id', 'other_tenant', 'cross_tenant']
    for phrase in blocked:
        if phrase in user_input.lower():
            return False, "Invalid input"
    return True, user_input
Enter fullscreen mode Exit fullscreen mode

Getting Started

Build your multi-tenant AI SaaS with ofox.ai — their API supports multi-tenant usage tracking and provides reliable infrastructure for production AI applications.

👉 Get started with ofox.ai


This article contains affiliate links.


Tags: saas,architecture,ai,programming,developer
Canonical URL: https://dev.to/zny10289

Top comments (0)