abstrakt
Models
Featured
Sora 2 Pro
Featured

Sora 2 Pro

OpenAI's most advanced video generation model with photorealistic output and complex scene understanding.

Veo 3.1
New

Veo 3.1

Google DeepMind's flagship video model with exceptional motion consistency and cinematic quality.

Kling 2.6
Popular

Kling 2.6

Latest Kling model with enhanced character consistency, longer duration support, and improved physics.

Active

100+ AI Models

Access the best AI models from multiple providers through one unified API. Switch models without changing code.

Browse all models
Tools
Featured
AI Image Generator
Popular

AI Image Generator

Create stunning images from text descriptions using FLUX, Stable Diffusion, and more.

Text to Video
New

Text to Video

Transform your ideas into cinematic AI videos with Sora, Veo, and Kling models.

Text to Speech

Text to Speech

Convert text to natural-sounding speech with 30+ voices and emotional expression.

Active

20+ AI Tools

Ready-to-use tools for image, video, and audio generation. No code required — just upload and create.

Explore all tools
Tutorials
Featured
Build Your First AI App
Start Here

Build Your First AI App

Your first AI generation in 5 minutes. Set up your API key and create your first image.

Text-to-Image Masterclass

Text-to-Image Masterclass

Master prompting techniques, model selection, and advanced settings for stunning results.

Text-to-Video Fundamentals

Text-to-Video Fundamentals

Learn to create cinematic AI videos with proper motion, pacing, and storytelling.

Active

Learn AI Generation

Step-by-step guides to master AI image, video, and audio creation. From beginner to advanced.

View all tutorials
Sandbox
Docs
TutorialsAdvancedBuilding a Multi-Tenant AI SaaS with Abstrakt
AdvancedUpdated Jan 25, 2026

Building a Multi-Tenant AI SaaS with Abstrakt

Learn how to build a production-ready multi-tenant SaaS application with AI features, including user isolation, usage tracking, billing integration, and scaling strategies.

DP
David Park
Senior Engineer
25 min read

Introduction

Building a multi-tenant AI SaaS requires careful architecture to ensure tenant isolation, fair resource allocation, and accurate billing. This tutorial walks through the complete implementation.

What You'll BuildA multi-tenant AI image generation SaaS with: - Tenant isolation and data separation - Per-tenant usage tracking and limits - Stripe billing integration - API key management - Admin dashboard

Architecture Overview

text
┌─────────────────────────────────────────────────────┐
│                    Your SaaS                         │
├─────────────────────────────────────────────────────┤
│  ┌─────────┐  ┌─────────┐  ┌─────────┐             │
│  │ Tenant A│  │ Tenant B│  │ Tenant C│             │
│  └────┬────┘  └────┬────┘  └────┬────┘             │
│       │            │            │                   │
│  ┌────▼────────────▼────────────▼────┐             │
│  │         API Gateway               │              │
│  │    (Auth, Rate Limiting)          │              │
│  └────────────────┬──────────────────┘             │
│                   │                                 │
│  ┌────────────────▼──────────────────┐             │
│  │        Usage Tracking             │              │
│  │    (Credits, Quotas, Billing)     │              │
│  └────────────────┬──────────────────┘             │
│                   │                                 │
└───────────────────┼─────────────────────────────────┘
                    │
           ┌────────▼────────┐
           │   Abstrakt API  │
           └─────────────────┘

Database Schema

sql
-- Tenants (Organizations)
CREATE TABLE tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(100) UNIQUE NOT NULL,
  plan VARCHAR(50) DEFAULT 'free',
  stripe_customer_id VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);

-- Tenant API Keys
CREATE TABLE api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID REFERENCES tenants(id),
  key_hash VARCHAR(64) NOT NULL,
  name VARCHAR(100),
  last_used_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Usage Records
CREATE TABLE usage_records (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID REFERENCES tenants(id),
  model VARCHAR(100) NOT NULL,
  credits_used INTEGER NOT NULL,
  metadata JSONB,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Monthly Usage Summaries
CREATE TABLE usage_summaries (
  tenant_id UUID REFERENCES tenants(id),
  month DATE NOT NULL,
  total_credits INTEGER DEFAULT 0,
  total_requests INTEGER DEFAULT 0,
  PRIMARY KEY (tenant_id, month)
);

Tenant Management

Tenant Service

typescript
// lib/tenants.ts
import { db } from './db';

export interface Tenant {
  id: string;
  name: string;
  slug: string;
  plan: 'free' | 'pro' | 'enterprise';
  stripeCustomerId?: string;
}

export const PLAN_LIMITS = {
  free: { monthlyCredits: 100, rateLimit: 10 },
  pro: { monthlyCredits: 5000, rateLimit: 100 },
  enterprise: { monthlyCredits: 50000, rateLimit: 1000 },
};

export async function getTenant(tenantId: string): Promise<Tenant | null> {
  const result = await db.query(
    'SELECT * FROM tenants WHERE id = $1',
    [tenantId]
  );
  return result.rows[0] || null;
}

export async function createTenant(data: Partial<Tenant>): Promise<Tenant> {
  const result = await db.query(
    `INSERT INTO tenants (name, slug, plan)
     VALUES ($1, $2, $3)
     RETURNING *`,
    [data.name, data.slug, data.plan || 'free']
  );
  return result.rows[0];
}

export async function getTenantUsage(tenantId: string, month: Date) {
  const result = await db.query(
    `SELECT * FROM usage_summaries 
     WHERE tenant_id = $1 AND month = $2`,
    [tenantId, month]
  );
  return result.rows[0] || { total_credits: 0, total_requests: 0 };
}

API Key Authentication

Key Generation and Validation

typescript
// lib/api-keys.ts
import crypto from 'crypto';
import { db } from './db';

const KEY_PREFIX = 'sk_';

export function generateApiKey(): { key: string; hash: string } {
  const randomBytes = crypto.randomBytes(24).toString('hex');
  const key = KEY_PREFIX + randomBytes;
  const hash = crypto.createHash('sha256').update(key).digest('hex');
  return { key, hash };
}

export async function createApiKey(tenantId: string, name: string) {
  const { key, hash } = generateApiKey();
  
  await db.query(
    `INSERT INTO api_keys (tenant_id, key_hash, name)
     VALUES ($1, $2, $3)`,
    [tenantId, hash, name]
  );
  
  // Return the actual key only once
  return { key, name };
}

export async function validateApiKey(key: string) {
  const hash = crypto.createHash('sha256').update(key).digest('hex');
  
  const result = await db.query(
    `SELECT ak.*, t.* 
     FROM api_keys ak
     JOIN tenants t ON ak.tenant_id = t.id
     WHERE ak.key_hash = $1`,
    [hash]
  );
  
  if (result.rows.length === 0) {
    return null;
  }
  
  // Update last used
  await db.query(
    'UPDATE api_keys SET last_used_at = NOW() WHERE key_hash = $1',
    [hash]
  );
  
  return result.rows[0];
}

Usage Tracking

Usage Service

typescript
// lib/usage.ts
import { db } from './db';
import { PLAN_LIMITS } from './tenants';

export async function recordUsage(
  tenantId: string,
  model: string,
  credits: number,
  metadata?: object
) {
  // Record individual usage
  await db.query(
    `INSERT INTO usage_records (tenant_id, model, credits_used, metadata)
     VALUES ($1, $2, $3, $4)`,
    [tenantId, model, credits, JSON.stringify(metadata || {})]
  );
  
  // Update monthly summary
  const month = new Date().toISOString().slice(0, 7) + '-01';
  await db.query(
    `INSERT INTO usage_summaries (tenant_id, month, total_credits, total_requests)
     VALUES ($1, $2, $3, 1)
     ON CONFLICT (tenant_id, month)
     DO UPDATE SET 
       total_credits = usage_summaries.total_credits + $3,
       total_requests = usage_summaries.total_requests + 1`,
    [tenantId, month, credits]
  );
}

export async function checkQuota(tenantId: string, plan: string) {
  const month = new Date().toISOString().slice(0, 7) + '-01';
  const usage = await db.query(
    'SELECT total_credits FROM usage_summaries WHERE tenant_id = $1 AND month = $2',
    [tenantId, month]
  );
  
  const used = usage.rows[0]?.total_credits || 0;
  const limit = PLAN_LIMITS[plan]?.monthlyCredits || 0;
  
  return {
    used,
    limit,
    remaining: Math.max(0, limit - used),
    exceeded: used >= limit,
  };
}

API Gateway

Main API Handler

typescript
// app/api/v1/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { validateApiKey } from '@/lib/api-keys';
import { checkQuota, recordUsage } from '@/lib/usage';
import { checkRateLimit } from '@/lib/rate-limit';

const MODEL_COSTS = {
  'flux-schnell': 1,
  'flux-dev': 2,
  'flux-pro': 5,
};

export async function POST(request: NextRequest) {
  // 1. Authenticate
  const apiKey = request.headers.get('Authorization')?.replace('Bearer ', '');
  if (!apiKey) {
    return NextResponse.json({ error: 'Missing API key' }, { status: 401 });
  }
  
  const tenant = await validateApiKey(apiKey);
  if (!tenant) {
    return NextResponse.json({ error: 'Invalid API key' }, { status: 401 });
  }
  
  // 2. Rate limit check
  const rateLimitResult = await checkRateLimit(tenant.id, tenant.plan);
  if (!rateLimitResult.allowed) {
    return NextResponse.json(
      { error: 'Rate limit exceeded', retry_after: rateLimitResult.retryAfter },
      { status: 429 }
    );
  }
  
  // 3. Quota check
  const body = await request.json();
  const model = body.model || 'flux-schnell';
  const cost = MODEL_COSTS[model] || 1;
  
  const quota = await checkQuota(tenant.id, tenant.plan);
  if (quota.remaining < cost) {
    return NextResponse.json(
      { error: 'Quota exceeded', usage: quota },
      { status: 402 }
    );
  }
  
  // 4. Forward to Abstrakt
  try {
    const result = await generateWithAbstrakt(model, body.input);
    
    // 5. Record usage
    await recordUsage(tenant.id, model, cost, {
      prompt: body.input.prompt?.slice(0, 100),
    });
    
    return NextResponse.json({
      success: true,
      result: result,
      usage: {
        credits_used: cost,
        credits_remaining: quota.remaining - cost,
      },
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Generation failed', details: error.message },
      { status: 500 }
    );
  }
}

async function generateWithAbstrakt(model: string, input: object) {
  const response = await fetch(
    `https://api.abstrakt.one/v1/models/${model}/run`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.ABSTRAKT_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ input }),
    }
  );
  
  return response.json();
}

Stripe Billing Integration

Subscription Management

typescript
// lib/billing.ts
import Stripe from 'stripe';
import { db } from './db';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

const PLAN_PRICES = {
  pro: 'price_pro_monthly_xxxxx',
  enterprise: 'price_enterprise_monthly_xxxxx',
};

export async function createCheckoutSession(
  tenantId: string,
  plan: 'pro' | 'enterprise'
) {
  const tenant = await db.query(
    'SELECT * FROM tenants WHERE id = $1',
    [tenantId]
  );
  
  let customerId = tenant.rows[0]?.stripe_customer_id;
  
  // Create Stripe customer if needed
  if (!customerId) {
    const customer = await stripe.customers.create({
      metadata: { tenant_id: tenantId },
    });
    customerId = customer.id;
    
    await db.query(
      'UPDATE tenants SET stripe_customer_id = $1 WHERE id = $2',
      [customerId, tenantId]
    );
  }
  
  // Create checkout session
  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    line_items: [{ price: PLAN_PRICES[plan], quantity: 1 }],
    success_url: `${process.env.APP_URL}/dashboard?upgraded=true`,
    cancel_url: `${process.env.APP_URL}/pricing`,
    metadata: { tenant_id: tenantId, plan },
  });
  
  return session.url;
}

// Webhook handler
export async function handleStripeWebhook(event: Stripe.Event) {
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await db.query(
        'UPDATE tenants SET plan = $1 WHERE id = $2',
        [session.metadata?.plan, session.metadata?.tenant_id]
      );
      break;
    }
    
    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      const customer = await stripe.customers.retrieve(
        subscription.customer as string
      );
      await db.query(
        'UPDATE tenants SET plan = $1 WHERE stripe_customer_id = $2',
        ['free', customer.id]
      );
      break;
    }
  }
}

Admin Dashboard

Usage Analytics API

typescript
// app/api/admin/analytics/route.ts
export async function GET(request: NextRequest) {
  const tenantId = request.headers.get('x-tenant-id');
  
  // Get usage by day for the last 30 days
  const dailyUsage = await db.query(
    `SELECT 
       DATE(created_at) as date,
       SUM(credits_used) as credits,
       COUNT(*) as requests
     FROM usage_records
     WHERE tenant_id = $1
       AND created_at > NOW() - INTERVAL '30 days'
     GROUP BY DATE(created_at)
     ORDER BY date`,
    [tenantId]
  );
  
  // Get usage by model
  const byModel = await db.query(
    `SELECT 
       model,
       SUM(credits_used) as credits,
       COUNT(*) as requests
     FROM usage_records
     WHERE tenant_id = $1
       AND created_at > NOW() - INTERVAL '30 days'
     GROUP BY model`,
    [tenantId]
  );
  
  return NextResponse.json({
    daily: dailyUsage.rows,
    byModel: byModel.rows,
  });
}

Deployment Considerations

Environment Variables

# .env.production
DATABASE_URL=postgres://...
ABSTRAKT_API_KEY=abs_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

Scaling Strategies

  1. Connection Pooling: Use PgBouncer for database connections
  2. Caching: Redis for rate limiting and frequently accessed data
  3. Queue: Use BullMQ for async job processing
  4. CDN: Cache generated images at the edge

Monitoring

typescript
// lib/monitoring.ts
export function trackMetric(name: string, value: number, tags: object) {
  // Send to your monitoring service (Datadog, etc.)
  console.log(`METRIC: ${name} = ${value}`, tags);
}

// Usage in API handler
trackMetric('api.request', 1, { 
  tenant: tenant.id, 
  model, 
  status: 'success' 
});

Complete Project Structure

text
├── app/
│   ├── api/
│   │   ├── v1/
│   │   │   └── generate/route.ts
│   │   ├── admin/
│   │   │   └── analytics/route.ts
│   │   └── webhooks/
│   │       └── stripe/route.ts
│   ├── dashboard/
│   │   ├── page.tsx
│   │   └── settings/page.tsx
│   └── pricing/page.tsx
├── lib/
│   ├── db.ts
│   ├── tenants.ts
│   ├── api-keys.ts
│   ├── usage.ts
│   ├── billing.ts
│   └── rate-limit.ts
└── middleware.ts

Next Steps

  • Add team member management
  • Implement usage alerts
  • Build customer-facing analytics
  • Add audit logging
#saas#multi-tenant#billing#architecture#production
PreviousVoice Cloning for Podcasts and Audiobooks
On This Page
  • Introduction
  • Architecture Overview
  • Database Schema
  • Tenant Management
  • Tenant Service
  • API Key Authentication
  • Key Generation and Validation
  • Usage Tracking
  • Usage Service
  • API Gateway
  • Main API Handler
  • Stripe Billing Integration
  • Subscription Management
  • Admin Dashboard
  • Usage Analytics API
  • Deployment Considerations
  • Environment Variables
  • Scaling Strategies
  • Monitoring
  • Complete Project Structure
  • Next Steps
Related Guides
Building an AI SaaS Product from Scratch

Build a complete AI-powered SaaS application step by step.

Webhook Configuration

Handle async AI jobs with webhook callbacks.

Was this page helpful?

abstrakt
abstrakt

The unified abstraction layer for the next generation of AI applications. Build faster with any model.

Start Here+
  • Quickstart
  • Get API Key
  • Try Playground
  • View Pricing
Image Tools+
  • AI Image Generator
  • Image to Image
  • Remove Background
  • Image Upscaler
  • Object Remover
  • Style Transfer
  • Image Enhancer
  • AI Art Generator
Video Tools+
  • Text to Video
  • Image to Video
  • AI Video Generator
  • Video Upscaler
  • Video Enhancer
  • Frame Interpolation
Audio Tools+
  • Text to Speech
  • Speech to Text
  • AI Music Generator
  • Voice Cloning
  • Audio Enhancer
  • Sound Effects
Tutorials+
  • Getting Started
  • Image Generation
  • Video Generation
  • Audio Generation
  • Advanced Topics
  • AI Glossary
  • All Tutorials
Models+
  • FLUX Schnell
  • FLUX Dev
  • Fast SDXL
  • Stable Diffusion 3
  • MiniMax Video
  • Kling AI
  • Ideogram
  • More Models
Company+
  • About Us
  • Pricing
  • Documentation
  • Tutorials
  • Blog
  • Contact
  • Changelog
  • Status
  • Careers
  • Privacy Policy
  • Terms of Service
  • Cookie Policy

Image Tools

  • AI Image Generator
  • Image to Image
  • Remove Background
  • Image Upscaler
  • Object Remover
  • Style Transfer
  • Image Enhancer
  • AI Art Generator

Video Tools

  • Text to Video
  • Image to Video
  • AI Video Generator
  • Video Upscaler
  • Video Enhancer
  • Frame Interpolation

Audio Tools

  • Text to Speech
  • Speech to Text
  • AI Music Generator
  • Voice Cloning
  • Audio Enhancer
  • Sound Effects

Tutorials

  • Getting Started
  • Image Generation
  • Video Generation
  • Audio Generation
  • Advanced Topics
  • AI Glossary
  • All Tutorials

Start Here

  • Quickstart
  • Get API Key
  • Try Playground
  • View Pricing

Models

  • FLUX Schnell
  • FLUX Dev
  • Fast SDXL
  • Stable Diffusion 3
  • MiniMax Video
  • Kling AI
  • Ideogram
  • More Models

Company

  • About Us
  • Pricing
  • Documentation
  • Tutorials
  • Blog
  • Contact
  • Changelog
  • Status
  • Careers
  • Privacy Policy
  • Terms of Service
  • Cookie Policy
abstrakt

The unified abstraction layer for the next generation of AI applications.

© 2026 abstrakt. All rights reserved.

SYS.ONLINE|API.ACTIVE|v1.2.0