0

Subdomain routing and dynamic domain routing

Intermediate5 min read·lv-06-010

Concept

Subdomain routing allows routing based on the HTTP Host header's subdomain. This enables tenant-based systems, API versioning by subdomain, or splitting your application across functional subdomains (api., admin., app.).

Route::domain(): Takes a domain pattern. {subdomain}.example.com captures the subdomain as a route parameter. Can be combined with any other route methods.

Dynamic domain routing ({account}.example.com): The captured {account} parameter is injected into controller methods or can be bound to an Eloquent model via route model binding.

Scoping route groups: Route::domain('api.{version}.example.com')->group(...) matches api.v1.example.com and api.v2.example.com and captures version.

Subdomain + prefix combination: You can combine domain() with prefix() and middleware() on the same group.

Local development: Test subdomain routing by adding entries to /etc/hosts: 127.0.0.1 tenant1.laravel.test. Alternatively use Valet's --secure with wildcard DNS, or a service like lvh.me (which resolves all subdomains to 127.0.0.1).

Multi-tenancy use case: Each tenant gets their own subdomain (acme.app.com, globex.app.com). The {tenant} parameter identifies the tenant and middleware sets up tenant context (database connection, config, etc.).

Code Example

php
<?php
use Illuminate\Support\Facades\Route;

// Static subdomain
Route::domain('api.example.com')->group(function() {
    Route::get('/users', [ApiController::class, 'users']);
    Route::post('/orders', [ApiController::class, 'createOrder']);
});

// Dynamic subdomain — capture subdomain as parameter
Route::domain('{account}.example.com')->group(function() {
    Route::get('/', [AccountController::class, 'dashboard']);
    Route::get('/settings', [AccountController::class, 'settings']);
});

// Controller receives subdomain parameter
class AccountController extends Controller
{
    public function dashboard(string $account): View
    {
        $tenant = Tenant::where('subdomain', $account)->firstOrFail();
        return view('dashboard', compact('tenant'));
    }
}

// Route model binding with subdomain
Route::domain('{tenant}.app.example.com')->group(function() {
    Route::get('/dashboard', [TenantController::class, 'dashboard']);
});

// Implicit binding (automatically finds Tenant by subdomain)
// In RouteServiceProvider:
Route::bind('tenant', function(string $value) {
    return Tenant::where('subdomain', $value)->firstOrFail();
});

// TenantController receives the model directly
class TenantController extends Controller
{
    public function dashboard(Tenant $tenant): View
    {
        return view('tenant.dashboard', compact('tenant'));
    }
}

// Generating URLs with subdomain
route('account.dashboard', ['account' => 'acme']);
// → https://acme.example.com/

// Subdomain + path prefix
Route::domain('{account}.example.com')
    ->prefix('admin')
    ->middleware(['auth', 'tenant'])
    ->group(function() {
        Route::resource('/users', AccountUserController::class);
    });