Реализация мультитенантной SaaS-архитектуры (Schema-per-Tenant)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Реализация мультитенантной SaaS-архитектуры (Schema-per-Tenant)
Сложный
~2-4 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1250
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    956
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    947

Представьте: SaaS-платформа для управления проектами с 500 клиентами. Каждый клиент создаёт проекты, добавляет участников, загружает файлы — все в общей базе данных. Одна ошибка в коде — и пользователь видит чужие данные: баги с пересечением tenant_id — частая проблема в системах с общими таблицами. Можно разнести клиентов по отдельным базам, но тогда каждая БД требует собственных бэкапов, миграций и мониторинга, что быстро становится неуправляемым. Компромисс — Schema-per-Tenant: каждая схема в общей базе PostgreSQL, но с полной изоляцией namespace. В этой статье мы делимся опытом реализации такого подхода с Prisma и Kysely.

PostgreSQL схемы работают как пространства имён: таблицы, индексы, функции в разных схемах не пересекаются. Одна база может содержать до 10 000 схем — этого достаточно для большинства B2B SaaS-продуктов. При этом администрирование остаётся единым: один бэкап, одна команда миграции, один мониторинг. Разберём, как организовать изоляцию данных, не жертвуя гибкостью.

Типовой процесс регистрации клиента: создаётся запись в общей таблице tenants, затем динамически создаётся новая схема и применяется начальная схема данных. Для этого нужно административное подключение с правами на DDL. Рассмотрим код и типичные подводные камни.

Как работает изоляция через схемы

PostgreSQL база:
  schema: public         → общие таблицы (tenants, plans)
  schema: tenant_acme    → данные клиента Acme
  schema: tenant_globex  → данные клиента Gloбех
  schema: tenant_initech → данные клиента Initech

PostgreSQL поддерживает до 10 000 схем в одной базе данных, что достаточно для большинства SaaS-продуктов. Каждая схема — отдельное пространство имён: таблицы, индексы, функции не пересекаются. При этом бэкап и миграции — одна операция на всю базу.

Как обеспечить изоляцию данных?

Создание схемы при регистрации

Типовой флоу: новый клиент → POST /api/tenants → создаём запись в public.tenants → выполняем DDL для новой схемы. Код на TypeScript:

// lib/tenant-provisioning.ts
import { db, adminDb } from './db';

export async function createTenantSchema(tenantSlug: string): Promise<string> {
  const schemaName = `tenant_${tenantSlug.replace(/-/g, '_')}`;

  // Транзакция в admin соединении
  await adminDb.$transaction(async (tx) => {
    await tx.$executeRawUnsafe(`CREATE SCHEMA "${schemaName}"`);

    await tx.$executeRawUnsafe(`
      SET search_path TO "${schemaName}";

      CREATE TABLE projects (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        name TEXT NOT NULL,
        created_at TIMESTAMPTZ DEFAULT NOW()
      );

      CREATE TABLE team_members (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        user_id TEXT NOT NULL,
        role TEXT NOT NULL DEFAULT 'member',
        joined_at TIMESTAMPTZ DEFAULT NOW()
      );

      CREATE INDEX ON projects (created_at DESC);
      CREATE INDEX ON team_members (user_id);
    `);
  });

  return schemaName;
}

Важно: DDL выполняется через административное соединение с правами на создание схем. Транзакция гарантирует атомарность — если что-то пошло не так, схема не появится.

Prisma: обход ограничений ORM

Prisma не умеет работать с несколькими схемами из коробки. Решение — динамический search_path через middleware. Создаём класс TenantPrismaClient, который перед каждым запросом переключает контекст на нужную схему:

// lib/tenant-client.ts
import { PrismaClient } from '@prisma/client';

export class TenantPrismaClient {
  private client: PrismaClient;
  private schema: string;

  constructor(schema: string) {
    this.schema = schema;
    this.client = new PrismaClient();

    // Middleware: устанавливаем search_path перед каждым запросом
    this.client.$use(async (params, next) => {
      await this.client.$executeRawUnsafe(
        `SET search_path TO "${this.schema}", public`
      );
      return next(params);
    });
  }

  get db() { return this.client; }

  async disconnect() {
    await this.client.$disconnect();
  }
}

// Фабрика с кэшем
const clients = new Map<string, TenantPrismaClient>();

export async function getTenantClient(tenantId: string): Promise<TenantPrismaClient> {
  if (clients.has(tenantId)) {
    return clients.get(tenantId)!;
  }

  const tenant = await masterDb.tenant.findUniqueOrThrow({
    where: { id: tenantId },
    select: { schemaName: true }
  });

  const client = new TenantPrismaClient(tenant.schemaName);
  clients.set(tenantId, client);
  return client;
}

Middleware предпочтительнее глобального search_path, потому что в production несколько тенантов обслуживаются одновременно. Каждый TenantPrismaClient держит своё соединение, переключение происходит только в рамках этого соединения — безопасно для других.

Альтернатива: Kysely

ORM с более гибкой поддержкой динамических схем — Kysely. Он позволяет задать search_path непосредственно при создании пула соединений:

import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';

function createTenantDb(schemaName: string) {
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
  });

  pool.on('connect', (client) => {
    client.query(`SET search_path TO "${schemaName}", public`);
  });

  return new Kysely({
    dialect: new PostgresDialect({ pool }),
  });
}

const tenantDb = createTenantDb('tenant_acme');
const projects = await tenantDb
  .selectFrom('projects')
  .selectAll()
  .orderBy('created_at', 'desc')
  .execute();

Kysely легче, чем Prisma, и даёт больше контроля. Но в проектах с уже внедрённой Prisma middleware-подход работает стабильно.

Миграции на все схемы

При изменении структуры таблиц нужно применить DDL ко всем схемам. Пишем скрипт, который проходит по списку тенантов и выполняет миграцию последовательно:

// scripts/migrate-schemas.ts
import { adminDb } from '../lib/db';

async function migrateAllSchemas(migration: string) {
  const tenants = await masterDb.tenant.findMany({
    select: { schemaName: true, slug: true }
  });

  for (const tenant of tenants) {
    console.log(`Migrating ${tenant.slug}...`);
    try {
      await adminDb.$executeRawUnsafe(`
        SET search_path TO "${tenant.schemaName}";
        ${migration}
      `);
    } catch (error) {
      console.error(`Failed: ${tenant.slug}`, error);
    }
  }
}

migrateAllSchemas(`
  ALTER TABLE projects ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;
  CREATE INDEX IF NOT EXISTS projects_archived_at ON projects (archived_at);
`);

Чтобы избежать простоев, миграции выполняются в рабочее окно с минимальной нагрузкой. Используем IF NOT EXISTS/IF EXISTS для идемпотентности. Если одна схема падает, остальные не блокируются.

Cross-tenant запросы для аналитики

Одно из преимуществ Schema-per-Tenant — возможность агрегировать данные по всем клиентам. Пример: подсчёт проектов по всем тенантам:

SELECT
  t.slug as tenant,
  COUNT(p.id) as project_count
FROM public.tenants t
CROSS JOIN LATERAL (
  SELECT id FROM tenant_acme.projects
  UNION ALL
  SELECT id FROM tenant_globex.projects
  -- ...динамически строится из списка тенантов
) p(id)
GROUP BY t.slug;

Для продакшена лучше использовать PL/pgSQL функцию, которая динамически строит запрос на основе активных схем.

Row Level Security (опционально)

Внутри схемы можно дополнительно разграничить доступ пользователей через RLS. Например, чтобы пользователь видел только свои проекты. Однако это имеет смысл только если в одной схеме работают несколько пользователей с разными правами. Для схемы на одного тенанта изоляция схемой достаточна.

Сравнение подходов мультитенантности

Критерий Database-per-Tenant Schema-per-Tenant Shared Table
Изоляция Полная Высокая Низкая
Администрирование Сложное (N баз) Среднее (1 база) Простое
Cross-tenant запросы Невозможны Возможны Лёгкие
Ограничения PostgreSQL 4 ГБ макс. баз (на практике ~1000) ~10 000 схем Практически нет
Сложность миграций N операций N операций 1 операция
Риск утечки данных Минимальный Низкий (при правильной настройке) Высокий

Schema-per-Tenant выбирают при клиентах от 50 до 2000, когда требуется изоляция, но хочется оптимизировать затраты на администрирование. Этот подход сокращает операционные расходы на инфраструктуру до 40% по сравнению с Database-per-Tenant.

Типовые проблемы и решения

Проблема Решение
Ошибка создания схемы для нового клиента Использовать транзакцию в административном соединении; откатывать схему при сбое
Необходимость переключения схемы в Prisma Middleware с установкой search_path перед каждым запросом
Низкая производительность cross-tenant запросов Использовать PL/pgSQL функцию с динамическим SQL, индексировать общие поля

Процесс работы: от идеи до деплоя

Мы реализуем мультитенантную архитектуру за 4–7 рабочих дней. Этапы:

  1. Аналитика — обсуждаем модель изоляции, логику регистрации, план миграций. Собираем требования к безопасности.
  2. Проектирование — рисуем ERD, определяем общие таблицы (tenants, планы) и схемы тенантов. Выбираем стек: Prisma или Kysely.
  3. Реализация — пишем код provisioning, middleware для ORM, скрипты миграций. Тестируем на нескольких тенантах.
  4. Тестирование — нагрузочные тесты, проверка изоляции, сценарии сбоев. Используем staging-окружение.
  5. Деплой — миграция существующих клиентов в новую архитектуру (если есть легаси). Запуск production.

Что входит в работу

  • Архитектурная документация (диаграммы, описание схем)
  • Исходный код модуля аренды и клиента БД
  • Миграционные скрипты и инструкции по развёртыванию
  • Доступы к репозиторию и CI/CD
  • 1 месяц базовой поддержки после сдачи

Почему выбирают нас

Мы имеем 7+ лет коммерческого опыта с PostgreSQL и SaaS-продуктами. Более 15 реализованных проектов с мультитенантной архитектурой. Гарантируем конфиденциальность данных — подписываем NDA. Предоставляем прозрачный тайм-трекинг и еженедельные отчёты. Свяжитесь с нами, чтобы обсудить ваш проект и получить индивидуальное предложение.

Разработка SaaS-платформ

Мы знаем эту боль наизусть. Запускаешь MVP с авторизацией и подпиской, а через полгода упираешься в архитектурные решения, которые нельзя откатить без переписывания половины кода. Multi-tenancy, биллинг, аудит логов, feature flags — каждый блок требует предварительного проектирования, иначе цена ошибки при масштабировании уходит в десятки человеко-месяцев, а в деньгах — от 3–5 млн ₽ на рефакторинг.

За 8 лет работы над SaaS-продуктами мы проверили на практике, какие решения работают, а какие превращают поддержку в ад. Ниже — архитектурные подходы, которые используем сами и рекомендуем клиентам.

Как мы строим multi-tenancy: изоляция без оверхеда

Первое, что решаем — схема разделения данных. Shared schema (tenant_id на каждой таблице) — наш стандартный выбор для большинства проектов. Все арендаторы в одной базе, миграции применяются разом, операционная сложность минимальна. В Laravel реализуем через Global Scope:

protected static function booted(): void
{
    static::addGlobalScope('tenant', function (Builder $builder) {
        $builder->where('tenant_id', TenantContext::current()->id);
    });
}

Глобальный скоуп — только первый уровень защиты. Обязательно добавляем Row-Level Security в PostgreSQL — она сработает, если приложение пропустит WHERE tenant_id = ?:

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::uuid);

Для enterprise-клиентов, которым нужна физическая изоляция, выделяем отдельную базу. Такой гибридный подход (shared + dedicated) используется в 80% зрелых SaaS: базовый продукт на shared schema, премиум — на отдельной инстанции. Мы внедряем его с первого спринта, чтобы не переписывать логику позже.

Модель multi-tenancy описана в Wikipedia: Multitenancy — рекомендуем ознакомиться для понимания trade-off'ов.

Почему биллинг — самый недооценённый блок

Upgrade посреди расчётного периода, downgrade с отложенным вступлением, истёкший trial, failed payment с grace period — Stripe Billing закрывает 90% сценариев из коробки. Обязательно обрабатываем вебхуки (customer.subscription.updated, invoice.payment_failed) с идемпотентным ключом — без него retry на клиенте приведёт к двойному списанию.

Для рынка СНГ — ЮKassa или Tinkoff recurring. API менее удобны, но покрывают требования 54-ФЗ.

Сравнение: переход с самописного биллинга на Stripe сокращает время разработки подписочной логики на 60%, а количество багов — на 80% (данные наших проектов). Экономия в деньгах для проекта среднего размера — до 2–3 млн ₽ на этапе разработки.

Onboarding: как не потерять пользователя до aha-moment

Технически onboarding — это wizard с persistent состоянием, который нельзя случайно пропустить. Таблица onboarding_steps с чек-листом, middleware редиректит на незавершённый шаг. После завершения — флаг в user settings, middleware отключается.

Критический нюанс: показывайте прогресс реального продукта, не абстрактные шаги. «Создайте первый отчёт» вместо «Завершите шаг 3 из 5». Мы используем drip-кампании через Customer.io или собственную очередь с отложенными jobs — если пользователь выполнил ключевое действие, следующее письмо не отправляется.

Feature flags и управление доступом

SaaS с тарифами требует гранулярного контроля. Не делайте if ($user->plan === 'pro') по всему коду — через месяц он станет неподдерживаемым. Вместо этого:

  • Backend: Gate + Policy с проверкой через таблицу features, связанную с планами.
  • Frontend: контекст с флагами, загружаемый при инициализации приложения.
  • Open-source инструменты: Unleash или Growthbook — UI для A/B-тестов и rollout.

Как защитить API от агрессивных клиентов

Rate limiting — must-have для публичного API. Один клиент может положить всех остальных. В Laravel используем Redis с sliding window counter:

Тариф Лимит Заголовки в ответе
Free 100 req/h X-RateLimit-Limit: 100
Pro 1 000 req/h X-RateLimit-Limit: 1000
Enterprise 10 000 req/h X-RateLimit-Limit: 10000

Каждый ответ содержит X-RateLimit-Remaining и X-RateLimit-Reset — клиенты рассчитывают на эти заголовки.

Аудит-логи и мониторинг: что, кто и когда

Без аудит-лога невозможно узнать, кто удалил проект или когда изменились настройки биллинга. Таблица audit_logs с индексами по (tenant_id, created_at) и (subject_type, subject_id). В Laravel — Observer'ы на ключевых моделях.

Пример реализации Observer для Model
class OrderObserver
{
    public function created(Order $order): void
    {
        AuditLog::create([
            'tenant_id' => $order->tenant_id,
            'user_id' => auth()->id(),
            'action' => 'created',
            'subject_type' => Order::class,
            'subject_id' => $order->id,
        ]);
    }
}

Мониторинг: Sentry для exception tracking, Grafana + Prometheus для метрик. Алерты на error rate > 5% и response time p95 > 2s.

Опыт нашей команды и гарантии

Над SaaS-платформами работают инженеры с 8+ летним опытом, за плечами — 50+ проектов, от стартапов до enterprise с миллионными нагрузками. Мы даём гарантию на архитектурные решения: если выбранный подход не масштабируется — перепроектируем за свой счёт.

Deliverables и гарантии

  • Документация архитектуры: схемы, ERD, sequence diagrams.
  • Настройка CI/CD (GitHub Actions / GitLab CI).
  • Доступы к репозиторию, стейджингу и продакшену.
  • Обучение команды: 2–3 сессии по код-ревью и runbook.
  • Post-launch поддержка 1 месяц.
  • Гарантия на архитектуру: бесплатный рефакторинг, если решение не проходит по нагрузке.

Процесс работы

  1. Discovery (1–2 недели) — аудит текущей архитектуры, скоуп MVP, приоритеты фич.
  2. Проектирование (1 неделя) — выбор стека, схема multi-tenancy, план биллинга.
  3. Разработка (4–12 недель) — спринты по 2 недели, демо после каждого.
  4. Тестирование (1 неделя) — нагрузочные тесты под target нагрузки, security audit.
  5. Деплой и обучение (1 неделя) — rollout, настройка мониторинга, передача документации.

Ориентиры по срокам

Этап Срок
MVP (core features + auth + billing) 12–16 недель
Полноценный продукт с admin panel 20–28 недель
Enterprise SaaS с multi-tenancy + audit 28–40 недель

Стоимость рассчитывается индивидуально — свяжитесь с нами, мы оценим проект за 2 дня. Закажите разработку под ключ: от проектирования до деплоя с гарантией архитектуры. Получите консультацию по архитектуре вашего продукта — первый час бесплатно.