Платформа для клінік має відповісти на два питання перед читанням картки пацієнта: якій організації належить запит і до яких записів може мати доступ цей користувач? Доменне ім'я допомагає відповісти на перше. Воно не відповідає на друге і само по собі не тримає фонового воркера в правильному контексті бази даних.
Ця стаття розглядає ці межі в Laravel API VetSpace. Це аналіз вихідного коду ідентифікації tenant, вибору сховища PostgreSQL, провіженінгу та аналізу в черзі. Це не твердження, що незалежна оцінка безпеки довела повну ізоляцію, або що дизайн досяг виміряного комерційного результату.
Корисний результат — відтворювана межа організації без окремої кодової бази для кожної клініки. Розв'язання домену обирає контекст клініки, провіженінг створює її сховище та початкові записи, а явне очищення запобігає тому, що задача в черзі залишає цей контекст активним. Спільні центральні записи все одно потребують власних перевірок авторизації.
Порівняйте моделі зберігання перед вибором
У дизайні зі спільною таблицею рядки несуть ідентифікатор організації. Існує одна схема для міграцій, а звітування між організаціями просте. Ціна — повсюдна вимога обмеження: кожен запит, зв'язок, експорт і фонова задача мають забезпечувати правильну межу організації. Політики бази даних можуть додати захист, але потребують ретельного дизайну привілеїв і контексту з'єднання.
Дизайн schema-per-tenant дає кожній організації власні таблиці всередині однієї бази PostgreSQL. Це уникає колізій між однойменними таблицями та підтримує специфічні для tenant міграції. Це не створює окремого сервера чи абсолютного бар'єра доступу. Документація схем PostgreSQL прямо пояснює, що привілеї визначають доступ між схемами і що search_path впливає на розв'язання імен.
Дизайн database-per-tenant розділяє цілі з'єднань і може спростити визначення процедур бекапу та відновлення окремих клієнтів. Він також збільшує операційну роботу: міграції, моніторинг, керування з'єднаннями та відновлення мають покривати багато баз. Бази на тому самому кластері все одно ділять інфраструктуру. Жоден із цих виборів сам по собі не демонструє гарантію регуляторної відповідності.
Що VetSpace фактично реалізує
VetSpace використовує stancl/tenancy і модель Tenant, що реалізує TenantWithDatabase, з concerns пакета HasDatabase та HasDomains. Пакет підтримує менеджери і баз даних, і схем PostgreSQL, як описано в його документації multi-database. Власний менеджер застосунку вирішує, який використати.
Важлива деталь у тому, що рішення не є просто «enterprise означає базу, все інше — схему». Наявне сховище має пріоритет. Цей вираз — уривок із UserRolePostgreSQLManager::resolveManager(); він залежить від перевірок існування цього класу та імпортованих класів менеджерів, тому це не самостійний скрипт провіженінгу:
$managerClass = match (true) {
$this->existsAsDatabase($name) => PostgreSQLDatabaseManager::class,
$this->existsAsSchema($name) => PostgreSQLSchemaManager::class,
$plan === 'enterprise' => PostgreSQLDatabaseManager::class,
default => PostgreSQLSchemaManager::class,
};
Менеджер перевіряє каталог баз даних PostgreSQL і метадані схем через центральне з'єднання. Лише коли не існує жодної форми сховища, план обирає початкове розміщення. Це зберігає старих tenant, створених із власною базою, навіть якщо їхній поточний план більше не enterprise. Зміна плану не мігрує їхні дані. Будь-яке переміщення сховища потребує окремо спроєктованої процедури копіювання, валідації та перемикання.
Розділіть центральний і клінічний API
Репозиторій має центральні маршрути та маршрути tenant-доменів. Центральні операції включають спільні записи власників і тварин та реєстр tenant. Клінічні операції включають філії, записи на прийом і візити. Це різні відповідальності за дані в межах одного застосунку, а не доказ незалежно розгорнутих сервісів.
На маршрутах tenant записаний масив middleware — не вся історія виконання. TenancyServiceProvider додає tenancy middleware на початок списку пріоритетів middleware Laravel. Тому ініціалізація домену виконується перед автентифікацією навіть там, де auth:sanctum з'являється раніше в оголошенні маршруту. Пошук токена має відбуватися через передбачене з'єднання; перестановка масиву маршруту без перевірки пріоритету виконання може дати оманливе виправлення.
Корисна перевірка використовує реальний hostname tenant, ініціалізує очікуване сховище та автентифікується токеном, створеним у цьому контексті. Потім повторіть запит до домену іншого tenant і переконайтеся у відмові. Перевірка лише існування домену не тестує межу автентифікації, а перевірка лише щасливого шляху пропускає випадковий доступ до записів іншої організації.
Провіженінг — це workflow, а не один insert
Провайдер реєструє pipeline TenantCreated: створити сховище, мігрувати його, заповнити початковими даними, створити головну філію та реплікувати власника. Код встановлює shouldBeQueued(false), тому цей pipeline синхронний. Описувати його як асинхронний онбординг суперечило б реалізації.
Порядок відображає реальні залежності: таблиці мають існувати перед їхніми початковими даними, а початкові записи клініки — перед тим, як клінікою можна користуватися. Це також створює випадки часткового збою. Якщо пізніший крок падає, рядок реєстру чи об'єкт сховища можуть уже існувати. Не можна припускати, що транзакція бази даних в одному з'єднанні скасовує кожну операцію провіженінгу між базами.
Для production-експлуатації я вимагав би задокументованого стану готовності та процедури відновлення з тестами збоїв після кожного суттєвого кроку. Це рекомендації, а не твердження, що перевірений pipeline реалізує повну систему компенсації. Практична бізнес-вигода виходить із знання, чи клініка готова, а не з трактування частково завершеного налаштування як успішної реєстрації клієнта.
Задача в черзі має встановити власний контекст
RunAiAnalysis несе два ідентифікатори: tenant та аналіз. Вона не припускає, що воркер успадкував запит браузера. Обробник шукає tenant у центральному контексті, повертається, якщо його немає, ініціалізує tenancy і потім шукає аналіз. Уривок основної обробки та очищення:
try {
tenancy()->initialize($tenant);
$analysis = AiAnalysis::find($this->analysisId);
if (! $analysis) {
return;
}
$service->analyze($analysis);
} finally {
if (tenancy()->initialized) {
tenancy()->end();
}
}
Це уривок тіла методу реальної задачі, а не повний клас. Попередній пошук tenant і впровадження залежностей залишаються необхідними. Блок finally виконується, коли обробка повертається або кидає виняток, запобігаючи тому, що задача залишає контекст tenant активним для наступної роботи. Це особливо важливо в повторно використаному процесі воркера.
Це не встановлює обробку exactly-once, автоматичне відновлення провайдера чи відповідну політику повторів. Вони залежать від конфігурації черги та сервісу аналізу. Відсутність tenant чи аналізу наразі спричиняє раннє повернення; чи має це також записувати операційну подію — окреме продуктове рішення.
Центральні записи також потребують авторизації
Ізоляція сховища захищає лише записи, збережені всередині обраного контексту tenant. VetSpace також має центральні записи власників і тварин, пов'язані з клініками. Трейт ScopesToClinic обмежує запити через асоціації clinic_owner та clinic_pet. Порожній список клінік змушений не збігатися ні з чим, а не знімати обмеження.
Ця відмінність важлива: центральний запис може зберігатися глобально, але бути видимим лише через авторизований зв'язок із клінікою. Рев'ю має перевіряти, які контролери використовують механізм обмеження, як обирається автентифікована клініка та як авторизуються записи. Існування трейта не доводить, що кожен ендпоінт використовує його правильно.
Що перевіряти і коли це не копіювати
Перед ухваленням дизайну протестуйте автентифікацію tenant-домену, читання та записи між клініками, повторно використаного воркера, що обслуговує двох tenant, частковий збій провіженінгу та відновлення одного tenant із бекапу. Перевіряйте і schema-backed, і database-backed tenant. Тест, що покриває лише початковий вибір плану, пропустить важливу гілку наявного сховища.
Бізнес-ефект, підтримуваний кодом, структурний: один застосунок може маршрутизувати роботу до окремих контекстів клінік і повторно використовувати визначену послідовність провіженінгу. Тут не подано жодного виміряного зменшення інцидентів, годин підтримки чи часу онбордингу. Такі твердження вимагали б операційних записів і визначеної базової лінії.
Для внутрішнього застосунку однієї організації ця механіка може бути непотрібною. Для малого SaaS із простою авторизацією та звітуванням спільні таблиці можуть бути простішими в експлуатації. Для сильнішої інфраструктурної ізоляції різні бази на одному сервері можуть все одно бути недостатніми. Починайте з вимог володіння, відновлення та доступу, потім обирайте найпростішу модель, що їх задовольняє.
Для обсягу реалізації, а не деталей архітектури, дивіться розробку на Laravel. Перша корисна розмова — про межу даних і випадки збоїв, а не про кількість tenant, яку нібито підтримує діаграма.