Multi-tenant з першого дня — ADR, який пропускає кожен соло-фаундер
Якщо ваш SaaS колись отримає другого клієнта, кожна таблиця має нести колонку tenant з першої міграції. Прикрутити це згодом — це не рефакторинг, а перепис.
У кожному соло-збудованому SaaS є момент, коли ви пишете перший
CREATE TABLE і кажете собі, що додасте колонку тенанта пізніше — коли
з'явиться другий клієнт. Не додасте. Це рішення розгалужується через
кожен join, кожен запит, кожен cache key — і коли ви помічаєте, «пізніше»
вже означає чотири тижні переписування, які нічого не випускають.
Я прийняв це рішення з самого початку на Futura AI (вбудований
AI-асистент для б'юті-салонів): кожна таблиця отримала tenant_id з
першої міграції, а короткий ADR пояснював чому — ще до того, як з'явилася
сама схема.
Скільки multi-tenant з першого дня реально коштує
Ціна — два додаткові рядки коду на таблицю і один фільтр на запит. Усе.
public sealed class Conversation
{
public Guid Id { get; init; }
public Guid TenantId { get; init; } // <-- ось це
// ... все інше
}
modelBuilder.Entity<Conversation>(b =>
{
b.HasIndex(x => new { x.TenantId, x.CreatedAt }); // <-- і це
b.HasQueryFilter(x => x.TenantId == _tenantContext.Current);
});
HasQueryFilter в EF Core означає, що ви пишете
db.Conversations.Where(...) так само як завжди; область тенанта
невидима в call-site, а відсутній фільтр стає compile-time проблемою
(немає контексту тенанта — немає записів), а не data-leak багом о 2-й
ночі.
Скільки коштує прикрутити це згодом
Якщо пропустите з першого дня, кожна таблиця, яку ви написали, потребує колонку, backfill, індекс і query filter — усе скоординоване через одну продакшн-міграцію без даунтайму. Ви також виявите, що:
- Ваші foreign key перетинають межі тенантів (
user_id, на який посилається таблиця без власногоtenant_id). - Ваші кеші ключуються id-сутності без префіксу тенанта, тож перший cross-tenant read після міграції віддає не той запис.
- Ваші background jobs запитують «усі розмови старші 30 днів» без жодного tenant scope.
Жодне з цього не є технічно складним. Усе це координаційно складне — і все це випливає на проді, де вже є клієнти. Вікно тихо це поправити зачинене.
ADR вміщається на серветці
Вам не потрібен довгий документ. Вам потрібен письмовий запис чому було прийнято це рішення — щоб ви-майбутній (або хто це успадкує) не скасував його під час рефакторингу.
# ADR-001: Кожна таблиця має tenant_id з першого дня
Status: Accepted
Date: 2025-12-XX
Decision
Кожна персистована сутність несе колонку `tenant_id`. Запити
скоупляться через EF Core query filters, прив'язані до
ambient ITenantContext.
Why
Прикрутити multi-tenancy після першого клієнта означає скоординовану
продакшн-міграцію по всіх таблицях і реальний ризик cross-tenant
витоків. Ціна заздалегідь — одна колонка і один фільтр на сутність.
Consequences
- Локальні seed дані мають мати дефолтний тенант.
- Інтеграційні тести мусять явно ставити ITenantContext.
- Один глобальний крок міграції перевіряє, що жодна сутність не
залишилась без колонки.
Це весь документ. Десять хвилин на написання — і чверть інженерного часу зекономлено того дня, коли він знадобиться.
Де це б'є, якщо цього не зробити
Я рятував проєкти, які пропустили цей крок. Патерн завжди один: працюючий додаток, клієнти, і беклог-item «підтримка кількох робочих просторів», який кікають уже шість місяців, бо насправді це означає переписати кожен запит.
Якщо у вашого продукту може бути другий клієнт — а в більшості B2B SaaS
може — найдешевший день додати tenant_id — це день, коли ви пишете
свій перший CREATE TABLE. Кожен наступний день дорожчий за
попередній.
Будуєте щось подібне?
Розкажіть, над чим працюєте. Беру невелику кількість проєктів одночасно.