Pengenalan Multi-Tenant di Go Fiber dan GORM
Pola multi-tenant mengizinkan satu aplikasi melayani banyak pelanggan (tenant) tanpa mencampur data. Dalam ekosistem Go, Go Fiber populer untuk HTTP server yang ringan, sementara GORM menjadi ORM pilihan karena fleksibilitasnya. Tantangannya adalah memastikan setiap request hanya melihat data tenant sendiri, dengan cara mengikat tenant ID ke layer query dan koneksi.
Pendekatan yang akan kita bahas mencakup: middleware yang menyuntikkan tenant ID/schema, scope GORM yang membatasi akses, manajemen koneksi tenant-spesifik, serta praktik untuk menjaga isolasi data. Fokusnya bukan sekadar teori, tapi implementasi nyata yang bisa langsung dipasang.
1. Menentukan Tenant ID dan Menyuntik lewat Middleware
Pertama, setiap request harus membawa identitas tenant, baik lewat header, JWT, atau subdomain. Middleware bertugas membaca identitas itu dan menyimpan di Context Fiber agar semua handler dan GORM scope dapat mengaksesnya.
Contoh middleware:
func TenantMiddleware(tenantRepo TenantRepository) fiber.Handler {
return func(c *fiber.Ctx) error {
tenantIdentifier := c.Get("X-Tenant-ID")
if tenantIdentifier == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant ID required"})
}
tenant, err := tenantRepo.FindByIdentifier(tenantIdentifier)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid tenant"})
}
c.Locals("tenant", tenant)
return c.Next()
}
}Penting: locally stored tenant harus berupa objek ringkas (ID, schema/koneksi) agar handler berikutnya tidak terus-menerus memanggil database tenant. Pastikan middleware dijalankan sebelum handler GORM.
2. Membatasi Query dengan Scopes Khusus Tenant
GORM menyediakan scopes untuk menambahkan kondisi secara global. Kita bisa memanfaatkan scope untuk mengikat semua query ke tenant_id atau schema.
Contoh scope tenant:
func TenantScope(tenantID uuid.UUID) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("tenant_id = ?", tenantID)
}
}Penggunaan dalam handler:
tenant := c.Locals("tenant").(*Tenant)
var orders []Order
if err := db.Scopes(TenantScope(tenant.ID)).Find(&orders).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}Keuntungan: Anda tidak perlu menyisipkan kondisi tenant_id di semua repository. Cukup definisikan scope, lalu gunakan di query utama.
Namun perlu diperhatikan: jika Anda menggunakan preload atau eager load, pastikan juga scope dijalankan di relasi agar data tidak bocor. GORM menyediakan Session(&gorm.Session{NewDB: true}) atau db.WithContext(ctx) untuk kondisi lebih kompleks.
3. Mengelola Koneksi Per Tenant
Ada dua pendekatan utama:
- Shared connection dengan tenant_id filter: satu database schema, semua tenant berbeda baris data. Lebih sederhana, namun memerlukan scope dan validasi ketat.
- Isolated schema/DB per tenant: setiap tenant punya schema/koneksi sendiri. Lebih aman tapi lebih kompleks di pool/connect.
Jika memilih schema terpisah, Anda bisa menyimpan informasi koneksi di tabel tenants. Middleware akan menyimpan koneksi GORM yang sudah dikonfigurasikan ke c.Locals("db").
func TenantDBMiddleware(dbPool *TenantDBPool) fiber.Handler {
return func(c *fiber.Ctx) error {
tenant := c.Locals("tenant").(*Tenant)
tenantDB, err := dbPool.GetDBForTenant(tenant)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "database connection unavailable"})
}
c.Locals("db", tenantDB)
return c.Next()
}
}TenantDBPool bisa berupa cache GORM DB per schema dengan singleton map plus mutex. Saat membuat koneksi baru, pastikan memanggil sql.DB configure seperti SetMaxOpenConns agar tidak bocor.
Saat query, gunakan c.Locals("db").(*gorm.DB) agar benar-benar memakai koneksi tenant tersebut. Jangan gunakan global gorm.DB jika schema bisa berbeda.
4. Menjaga Isolasi Data dan Menghindari Bocor
Beberapa prinsip dan cek:
- Validasi middleware: Setiap request harus melewati middleware tenant. Konsisten gunakan
c.Use(TenantMiddleware...). - Audit log dan context trace: Rekam tenant ID saat error untuk troubleshoot. Gunakan logger yang menambahkan field
tenant. - Scope & context double-check: Saat menulis query kompleks, jalankan
db.Statement.SQL.String()selama debugging untuk memastikanWHERE tenant_idbenar. - Test isolasi: Tulis integration test untuk memastikan tenant A tidak melihat data B. Gunakan dataset terpisah lalu assert.
Trade-off: schema per tenant mengurangi risiko bocor, tapi menambah overhead provisioning (membuat schema, migrasi). Shared schema lebih mudah dipasang tetapi mensyaratkan disiplin scope. Pilih berdasarkan kebutuhan konsistensi dan jumlah tenant.
5. Tips Debug dan Praktik Terbaik
Debugging: Fiber menyediakan c.Context() untuk menambahkan log. Pastikan saat terjadi error database, Anda sudah log tenant ID agar bisa melacak per tenant.
Implementasi migrasi multi-tenant: Jika memakai schema terpisah, buat mekanisme migrasi yang dapat dijalankan per tenant, misalnya:
for _, tenant := range tenants {
db, _ := dbPool.GetDBForTenant(tenant)
db.AutoMigrate(&Order{}, &Product{})
}Testing: Gunakan tabel temporer untuk tiap tenant di environment test agar order/billing tidak tercampur. Kunci adalah membuat helper NewTenantContext yang siap di pake di unit test.
Limitasi: Middleware yang menyuntik tenant ID menuntut header/claim selalu valid. Perubahan otentikasi (token/SSO) harus sinkron. Hal lain, c.Locals bersifat request scope; pastikan tidak digunakan di goroutine yang menyimpan pointer lama.
Kesimpulan
Mengelola multi-tenant dengan Go Fiber dan GORM memerlukan koordinasi middleware, scope query, dan koneksi database agar data tetap terisolasi. Dengan menanamkan tenant ID di middleware, membatasi query lewat scope spesifik, dan (opsional) membuat koneksi per tenant, Anda dapat memenuhi kebutuhan keamanan dan performa. Selalu sertakan monitoring, logging tenant-aware, dan suite test untuk menghindari data bocor antar tenant. Pendekatan yang tepat bergantung pada kompleksitas tenant dan kebijakan isolasi data, jadi pilihlah kombinasi scope vs schema berdasarkan kebutuhan nyata.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!