Local Subdomain Development
For local development you don't need to edit /etc/hosts, set up dnsmasq, or run a DNS server. Every modern browser resolves *.localhost to 127.0.0.1 automatically — this is specified by RFC 6761. The docker compose dev stack uses this.
What works out of the box
With bin/dev-setup.sh running:
| URL | What happens |
|---|---|
http://localhost:3000 |
Native Next.js dev server — no tenant, hits landlord/platform UI |
http://demo.localhost:3000 |
Same Next.js server — window.location.hostname is demo.localhost → frontend extracts demo as the tenant → sends X-Tenant: demo on API calls |
http://acme.localhost:3000 |
Same, but for tenant acme |
http://localhost:8000/api/v1/... |
Direct backend API — expects X-Tenant header or hits central routes |
http://demo.localhost:8000/api/v1/... |
Backend sees Host: demo.localhost → Stancl's InitializeTenancyByDomain resolves demo |
No configuration, no DNS edits. All major browsers (Chrome, Firefox, Safari, Edge) resolve *.localhost to 127.0.0.1 or ::1 without asking the network.
What does NOT work locally
Non-browser tools sometimes don't resolve *.localhost. The RFC says they should, but:
curlworks ✓httpieworks ✓- Native macOS
pingmay skip DNS and go straight to.localmDNS — usecurlinstead if you need to hit a subdomain from the terminal wgetworks ✓- Postman / Insomnia work ✓
Testing the subdomain flow end-to-end
-
Bring up docker dev + native frontend:
bin/dev-setup.sh cd frontend && bun run dev -
Hit the tenant subdomain:
curl -sS -X POST http://demo.localhost:8000/api/v1/auth/login \ -H "Content-Type: application/json" \ -H "X-Tenant: demo" \ -d '{"email":"owner@demo.com","password":"password"}' -
In the browser: open
http://demo.localhost:3000/login— the frontend auto-extractsdemofromwindow.location.hostnameand includes theX-Tenantheader on every API request. You don't need to set anything.
Adding a new local tenant
Create a tenant directly in the central DB:
docker compose -p autocom-dev exec app php artisan tinker --execute='
use App\Models\Tenant;
use App\Models\User;
use App\Models\TenantUser;
use Illuminate\Support\Facades\Hash;
$t = Tenant::create(["id"=>"localshop", "name"=>"Local Shop", "plan"=>"free"]);
$t->domains()->create(["domain" => "localshop"]);
$u = User::create([
"name" => "Local Admin",
"email" => "admin@localshop.com",
"password" => Hash::make("password"),
"email_verified_at" => now(),
]);
TenantUser::create([
"tenant_id" => "localshop",
"user_id" => $u->id,
"role_id" => \Spatie\Permission\Models\Role::where("name","owner")->first()->id,
"status" => "active",
]);
echo "Ready → http://localshop.localhost:3000" . PHP_EOL;
'
Browse to http://localshop.localhost:3000 → log in → you're in the new tenant's context.
Why your prod setup will "just work"
The browser's *.localhost shortcut lets you exercise the exact same subdomain-based tenant resolution that runs in production. The Laravel middleware, Stancl bootstrappers, and frontend subdomain extraction all run unchanged. When you move to prod, the only piece that changes is where DNS resolves — see K8s Wildcard Setup for the single wildcard A record that replaces *.localhost.
Put differently: if a tenant flow works with demo.localhost:3000, it works with demo.autocom.app without code changes.
Debugging tenant resolution
If a subdomain isn't resolving to the right tenant, walk the chain in reverse:
# 1. Is the Host header reaching the backend?
curl -sS -v http://demo.localhost:8000/api/v1/install/check 2>&1 | grep -i "host:"
# Expect: Host: demo.localhost:8000
# 2. Does the `domains` table have a row for `demo`?
docker compose -p autocom-dev exec pgsql \
psql -U autocom -d autocom -c "SELECT domain, tenant_id FROM domains WHERE domain = 'demo';"
# 3. Does the tenant exist?
docker compose -p autocom-dev exec pgsql \
psql -U autocom -d autocom -c "SELECT id, name, status FROM tenants WHERE id = 'demo';"
# 4. Does Laravel see it?
docker compose -p autocom-dev exec app php artisan tinker --execute='
echo \App\Models\Tenant::find("demo")?->name ?? "not found";
'
If all four succeed and the frontend still can't log in, check the browser's network tab for the outgoing X-Tenant header.