Membangun aplikasi multi-tenant dengan stack monolit modern seperti Laravel dan Inertia.js menuntut keputusan arsitektur database sejak tahap awal. Pendekatan yang dipilih mempengaruhi kompleksitas routing, manajemen koneksi database, konsumsi memori server, hingga keamanan data pada response layer Inertia.
Dua pola dominan yang umum digunakan adalah Shared Database (pemisahan berbasis kolom tenant_id) dan Isolated Database (pemisahan fisik database atau skema terpisah). Keduanya memiliki trade-off mendasar antara efisiensi sumber daya dan batas isolasi data.
1. Tenant Resolution Layer pada Request Lifecycle
Sebelum query database dieksekusi atau response Inertia di-render, aplikasi harus menentukan tenant aktif. Pada arsitektur monolitik Inertia, resolusi umumnya dilakukan via HTTP middleware menggunakan subdomain (tenant.app.com) atau path prefix (app.com/{tenant}).
Resolver bertugas memvalidasi identitas tenant, menginisialisasi state aplikasi, dan memastikan request context diisolasi secara benar sebelum masuk ke controller.
namespace App\Http\Middleware;
use Closure;
use App\Models\Tenant;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ResolveTenant
{
public function handle(Request $request, Closure $next): Response
{
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
$tenant = Tenant::where('slug', $subdomain)->first();
if (! $tenant) {
abort(404, 'Tenant not found');
}
app()->instance(Tenant::class, $tenant);
return $next($request);
}
}Tantangan spesifik pada Inertia muncul saat terjadi partial reloads atau request dengan header X-Inertia. Jika middleware tenant resolver gagal mengevaluasi context pada request asinkron tersebut, aplikasi berisiko me-render komponen frontend dengan data tenant yang tidak konsisten atau fallback ke context default.
2. Pola Shared Database (Row-Level Scoping)
Pada pendekatan Shared Database, seluruh tenant berada dalam satu instance dan skema database yang sama. Pemisahan data dilakukan secara logis menggunakan kolom tenant_id di setiap tabel tenant-specific.
Mekanisme dan Implementasi
Isolasi ditegakkan via Eloquent Global Scope atau PostgreSQL Row Level Security (RLS). Setiap query otomatis diinjeksi klausul WHERE tenant_id = ?.
namespace App\Models\Scopes;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if (app()->bound(Tenant::class)) {
$builder->where($model->getTable() . '.tenant_id', app(Tenant::class)->id);
}
}
}Trade-off Teknis Shared DB
- Efisiensi Connection Pool: Sangat efisien. Seluruh tenant menggunakan satu pool koneksi database global. Beban koneksi ke database server tetap stabil seiring bertambahnya jumlah tenant.
- Risiko Data Leak: Sangat tinggi jika terjadi developer error. Penggunaan raw queries (
DB::raw), bypass scope yang tidak disengaja (withoutGlobalScopes()), atau relasi model yang lupa memanggil global scope dapat menyebabkan data lintas tenant bocor dalam satu query. - Skalabilitas Data: Ukuran indeks tabel membengkak drastis saat skala data mencapai ratusan juta baris, memperlambat proses vacuuming/maintenance database secara keseluruhan.
3. Pola Isolated Database (Database atau Skema Terpisah)
Pola Isolated Database memisahkan data tenant secara fisik ke database tersendiri (misal: tenant_1_db, tenant_2_db) atau secara logis ke skema terpisah (misal pada PostgreSQL schema search path).
Mekanisme Dynamic Connection Switching
Middleware tenant resolver harus mengonfigurasi ulang koneksi database runtime sebelum handler controller berjalan.
namespace App\Http\Middleware;
use Closure;
use App\Models\Tenant;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
class SwitchTenantDatabase
{
public function handle($request, Closure $next)
{
$tenant = app(Tenant::class);
// Mutasi konfigurasi koneksi tenant secara dinamis
Config::set('database.connections.tenant.database', $tenant->database_name);
DB::purge('tenant');
DB::reconnect('tenant');
DB::setDefaultConnection('tenant');
$response = $next($request);
// Putus koneksi untuk mencegah kebocoran state antar request (terutama pada Octane/Swoole)
DB::disconnect('tenant');
return $response;
}
}Trade-off Teknis Isolated DB
- Connection Pool Exhaustion: Setiap database tenant yang dibuka secara dinamis membutuhkan koneksi terpisah. Jika aplikasi dijalankan di atas multi-worker (seperti PHP-FPM dengan pool besar atau Laravel Octane), pooling koneksi ke ratusan database berbeda dapat melampaui batas
max_connectionsserver database dalam hitungan detik. - Overhead Provisioning: Pembuatan tenant baru membutuhkan eksekusi DDL (Create Database/Schema) dan pipeline migrasi baru, bukan sekadar
INSERTrecord baru. - Isolasi Data Mutlak: Kebocoran data akibat kesalahan klausa query terminimalisasi secara drastis karena engine database secara fisik tidak menyimpan data entitas lain dalam scope koneksi yang aktif.
4. Vektor Kebocoran Data pada Inertia Shared Props
Pada aplikasi Inertia.js, backend mengirimkan payload props ke frontend dalam format JSON melalui data attribute tag HTML atau response JSON. File middleware bawaan HandleInertiaRequests menyediakan fungsi share() untuk data global seperti user login, session flash, dan tenant metadata.
Peringatan: Kesalahan konfigurasi shared props pada monolit multi-tenant dapat membocorkan state tenant secara global, terlepas dari apakah arsitektur DB yang digunakan Shared atau Isolated.
Hindari caching shared props yang melibatkan tenant context di level application singleton atau static property, terutama bila menggunakan runtime persisten (Laravel Octane, RoadRunner). Gunakan lazy evaluation via closure agar data diikat secara eksklusif ke siklus request yang aktif:
public function share(Request $request): array
{
return [
...parent::share($request),
'tenant' => fn () => app()->bound(Tenant::class) ? [
'id' => app(Tenant::class)->id,
'name' => app(Tenant::class)->name,
'slug' => app(Tenant::class)->slug,
] : null,
'auth.user' => fn () => $request->user()
? $request->user()->only('id', 'name', 'email')
: null,
];
}Jika data user atau tenant di-cache lintas request tanpa isolasi worker context, user dari Tenant B yang mengakses endpoint Inertia dapat menerima payload props milik Tenant A pada partial page reload.
5. Evaluasi Biaya Operasional dan Maintainability
Migrasi Skema Skala Besar
- Shared DB: Migrasi skema berjalan sederhana melalui satu kali eksekusi
php artisan migrate. Namun, modifikasi tabel raksasa (ALTER TABLE pada ratusan juta baris) membutuhkan tooling zero-downtime seperti gh-ost atau pg_repack agar tidak mengunci tabel operasional seluruh tenant. - Isolated DB: Migrasi harus diulang secara sekuensial atau paralel ke ratusan/ribuan database tenant. Kegagalan migrasi di tengah jalan menyebabkan disparitas skema (sebagian tenant berada di versi skema lama, sebagian di versi skema baru), membutuhkan mekanisme distributed migration tracker yang kompleks.
Granularitas Backup dan Restore
Pada Shared DB, melakukan point-in-time recovery (PITR) untuk satu tenant yang tidak sengaja menghapus datanya membutuhkan restore seluruh database ke server staging sementara, kemudian mengekstraksi record tenant tersebut via export query log. Pada Isolated DB, proses restore bersifat granular: cukup dump dan restore file database spesifik tenant tersebut tanpa mengganggu tenant lain.
6. Matriks Keputusan Arsitektur
| Kriteria Evaluasi | Shared Database (tenant_id) | Isolated Database (DB/Schema) |
|---|---|---|
| Isolasi Data | Logis (Beresiko developer error) | Fisik (Batas data tegas) |
| Efisiensi Koneksi DB | Tinggi (Single pool) | Rendah (Rawan pool exhaustion) |
| Kompleksitas Migrasi | Rendah (Single migration run) | Tinggi (Multi-tenant orchestrator) |
| Backup/Restore Granular | Sangat rumit | Sangat mudah (Per-file basis) |
| Overhead Infrastruktur | Minimal | Tinggi saat jumlah tenant besar |
| Kesesuaian Regulasi (HIPAA/GDPR) | Memerlukan audit logic ketat | Tinggi (Ideal untuk enterprise tier) |
Pilih Shared Database jika aplikasi menargetkan model B2C atau B2B micro/SME skala besar dengan margin biaya infrastruktur yang ketat, dan tim memiliki automated testing suite yang memvalidasi kebocoran scope pada setiap endpoint. Pilih Isolated Database jika compliance regulasi mewajibkan segregasi fisik, data retention policy per-klien berbeda secara radikal, atau nilai kontrak per-tenant cukup besar untuk membiayai overhead operasional infrastrukturnya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!