Custom Domain SaaS di Laravel dengan SSL Otomatis lewat Cloudflare
Cepat atau lambat, pelanggan SaaS multi-tenant akan minta tokonya tampil di shop.mybrand.com, bukan mybrand.yoursaas.com. Untuk urusan custom domain SaaS di Laravel ini, seorang developer merilis package open-source bernama a-khalid/cloudflare-saas-domains. Cloudflare for SaaS memang mengurus sertifikatnya, tapi sisa pekerjaannya tetap jatuh ke aplikasimu, dan package ini mengerjakan bagian itu.
Kenapa lebih rumit dari satu API call
Membuat custom hostname di API Cloudflare cuma butuh satu request. Masalahnya ada di sekitar request itu: custom domain adalah state machine yang bisa berjalan berhari-hari dan bergantung pada orang yang mengedit DNS di tempat lain. Ada lima hal yang sering bikin tersandung:
- Ada dua validasi, bukan satu. Kepemilikan hostname dan penerbitan sertifikat terpisah, dengan token dan status masing-masing. Domain baru siap kalau keduanya active dan DNS mengarah ke kamu.
- Record yang dibutuhkan mungkin belum ada. Cloudflare membuat token validasi sertifikat secara asinkron, jadi respons saat create bisa datang tanpa token. UI-mu harus bisa menangani kondisi belum siap, silakan tanya lagi nanti.
- Polling mempersulit event. Kalau domain dicek tiap lima menit, email ucapan selamat untuk merchant harus terkirim sekali saat transisi sebenarnya, bukan di setiap poll. Dua worker yang polling bersamaan juga tidak boleh mengirimnya dua kali.
- Gagal sering kali normal. Error does not CNAME to this zone wajar muncul sebelum merchant selesai mengatur DNS. Hostname yang dihapus Cloudflare setelah masa validasi habis adalah masalah sungguhan. Kodemu harus bisa membedakan keduanya.
- Request harus menemukan tenant yang benar. Setiap request datang membawa hostname yang belum pernah dikenal aplikasimu. Pencarian ini jalan di tiap request, jadi perlu cache, dan cache itu harus langsung dibersihkan begitu status domain berubah.
Alur lengkap di Laravel
Package ini butuh PHP 8.3+ dan Laravel 12 atau 13. Untuk Laravel 10 dan 11 tersedia rilis terpisah. Di sisi Cloudflare, kamu perlu zone dengan Cloudflare for SaaS aktif, fallback origin yang active, dan API token dengan izin SSL and Certificates Write. Alurnya terbagi enam langkah:
- 1. Install dan konfigurasi. Jalankan composer require a-khalid/cloudflare-saas-domains, publish config dan migration lewat vendor:publish, lalu php artisan migrate. Isi variabel env CF_SAAS_API_TOKEN, CF_SAAS_ZONE_ID, CF_SAAS_FALLBACK_ORIGIN, CF_SAAS_CNAME_TARGET, dan CF_SAAS_BLOCKED_DOMAINS. Setelah itu, tambahkan trait HasCustomDomains ke model tenant, misalnya Merchant.
- 2. Tambah domain. Panggil CloudflareSaasDomains::add dengan model merchant dan hostname dari request. Hostname dinormalisasi dan divalidasi sebelum ada panggilan API, termasuk konversi domain internasional ke punycode. Empat exception aman ditampilkan ke merchant: InvalidHostnameException, ApexDomainNotAllowedException, BlockedHostnameException, dan DomainAlreadyClaimedException. Error API sebaiknya diserahkan ke exception handler karena pesannya memuat detail internal. Memanggil add dua kali juga aman: kalau hostname sudah dimiliki merchant, kamu menerima domain yang ada tanpa panggilan API dan tanpa event.
- 3. Beri tahu merchant apa yang harus dilakukan. CloudflareSaasDomains::instructions($domain) menghasilkan daftar record DNS lewat method required(). Untuk domain baru, hasilnya kira-kira seperti ini: CNAME shop.mybrand.com customers.your-saas.com, TXT _cf-custom-hostname.shop.mybrand.com 9e65b562-ab7c-479c-9c9a-9e6514632c5e, dan TXT _acme-challenge.shop.mybrand.com PzbB9BB8zprTZlQ3deWWg2ad5o7j8f1s. Satu sertifikat bisa membutuhkan dua nilai TXT di bawah nama _acme-challenge yang sama, jadi minta merchant menambahkan semua record yang tertera. Daftar ini mengikuti status domain, sehingga merchant hanya melihat yang masih kurang. Tiga flag menangani kasus rumit: awaitingValidationRecords berarti token belum siap, retryRequired berarti validasi gagal, dan hostnameGone berarti Cloudflare sudah tidak memiliki hostname tersebut.
- 4. Biarkan status memperbarui diri. Jadwalkan cf-domains:sync tiap lima menit dan cf-domains:sync --all setiap hari, keduanya dengan withoutOverlapping. Perintah pertama mengecek domain yang masih pending. Yang kedua ikut mengecek domain yang sudah live, sehingga hostname yang berhenti mengarah ke kamu atau sertifikat yang kedaluwarsa ketahuan. Untuk tombol Verify now, panggil CloudflareSaasDomains::retryValidation($domain).
- 5. Bereaksi sekali, bukan di setiap poll. Ada lima event: DomainAdded, DomainVerified, SslActivated, DomainFailed, dan DomainRemoved. Masing-masing hanya menyala saat state benar-benar berubah, dan baris database dikunci selama perubahan diterapkan, jadi dua poll bersamaan tidak bisa sama-sama memicunya. Contohnya, listener SslActivated bisa mengirim notifikasi DomainIsLive ke tenant kalau domain isLive() dan event itu bukan renewal. Listener DomainFailed cukup mencatat alasan dan hostname ke log.
- 6. Cari tenant di setiap request. Bungkus route dengan middleware cf-domains.tenant, lalu ambil tenant dari $request->attributes->get('tenant'). Hanya domain yang active yang dikenali, dan host lain mendapat 404. Hasil pencarian di-cache dan dibersihkan saat status domain berubah.
Kalau kamu sedang membangun platform multi-tenant di Laravel, misalnya layanan toko online atau website builder, package ini bisa memangkas banyak kode pendukung yang biasanya ditulis sendiri dan sering salah di kasus-kasus pinggir seperti event ganda atau cache yang basi.