Multi-Tenant Host-Header Routing
HomeStar’s multi-tenant architecture automatically scopes lead capture and public listing/search results to the correct brokerage based on the request domain, enabling white-label deployments without frontend configuration.
How It Works
Section titled “How It Works”Routing is automatic and transparent across public endpoints:
- Client submits form / requests listings on
kroetch.homestar.ink - Request includes
X-Forwarded-Host: kroetch.homestar.ink(set by Caddy) - API resolves host via
brokerage_domains→brokerage_id: 2 - Lead is created — or listings are filtered — scoped to
brokerage_id: 2
No frontend configuration is required. The shared resolver lives in
idx_api/utils/brokerage_host.py and is used by every public router
that needs per-tenant scoping (/api/contact, /api/listings, search
facets, etc.).
Platform Hosts (Cross-Tenant)
Section titled “Platform Hosts (Cross-Tenant)”A short list of hosts intentionally serves every brokerage’s listings together rather than being scoped to one tenant. These are the unbranded platform surfaces:
homestar.ink— marketing sitewww.homestar.inkapp.homestar.ink— unbranded SaaS app where logged-in users land before picking a tenant
When a request arrives on one of these hosts, resolve_brokerage_id_from_host
returns None and the listing query runs unfiltered (or rejects
public lead submissions outright, since “platform host” is not a
valid brokerage to route leads to).
Domain Resolution
Section titled “Domain Resolution”Header Priority
Section titled “Header Priority”The resolver checks headers in this order:
X-Forwarded-Host— Set by Caddy / any TLS-terminating reverse proxyHost— Standard HTTP header, used as fallback for direct API traffic
X-Forwarded-Host is checked first because the Astro SSR
middleware proxies /api/* to http://idx-api:8000 and replaces
Host with idx-api:8000 along the way. Caddy preserves the
user-typed hostname in X-Forwarded-Host, so that’s where the real
tenant identity lives by the time the API sees the request.
The host string is normalized before lookup: port stripped, lowercased,
and a leading www. removed so a domain row stored as elevateidaho.com
matches a request to www.elevateidaho.com.
Domain Lookup
Section titled “Domain Lookup”Domains are stored in a separate brokerage_domains table so one
brokerage can own multiple hostnames (apex, www, branded subdomain):
SELECT brokerage_idFROM brokerage_domainsWHERE domain = 'kroetch.homestar.ink';What happens when no row matches depends on the endpoint:
- Public listings / search (
/api/listings,/api/listings/facets) return results with no brokerage filter applied. Useful for the platform-host case, harmless for a misconfigured tenant. - Contact form (
/api/contact) returns404 Not Foundwith"Domain '...' is not configured."— we deliberately won’t accept an inbound lead we can’t route to anyone.
The resolver also returns None (no scoping) when the matched brokerage
has been soft-deleted (disabled_at IS NOT NULL), so disabling a
brokerage immediately stops both lead capture and tenant-scoped
listing visibility for that domain.
Why This Approach?
Section titled “Why This Approach?”Benefits
Section titled “Benefits”Zero frontend configuration:
- No hardcoded brokerage IDs in JavaScript
- Same frontend code deployed across all brokerages
- White-label sites work immediately after DNS configuration
Security:
- Brokerages cannot accidentally access each other’s leads
- Domain ownership proves authorization
- No API key management for public endpoints
Scalability:
- Add new brokerages by configuring DNS and database entry
- No code changes required per deployment
- Supports unlimited white-label instances
Trade-offs
Section titled “Trade-offs”Domain required:
- Cannot submit leads without a valid domain mapping
- Testing requires domain configuration or hosts file modification
DNS dependency:
- Domain must resolve to the API server
- DNS changes have propagation delay
No IP-based routing:
- Cannot route based on client IP geolocation
- Requires domain per brokerage (subdomains acceptable)
Error Scenarios
Section titled “Error Scenarios”Domain Not Configured
Section titled “Domain Not Configured”If the request domain has no brokerage mapping:
{ "detail": "Domain 'unknown.example.com' is not configured. Unable to process lead."}Status: 404 Not Found
Resolution: Add domain mapping in admin panel or database.
Missing Host Header
Section titled “Missing Host Header”If neither Host nor X-Forwarded-Host is present:
{ "detail": "Missing Host header"}Status: 400 Bad Request
Resolution: Check reverse proxy configuration. Ensure headers are forwarded correctly.
Brokerage Disabled
Section titled “Brokerage Disabled”If the brokerage exists but is disabled:
{ "detail": "Brokerage not found"}Status: 404 Not Found
Resolution: Re-enable brokerage in admin panel.
Testing Locally
Section titled “Testing Locally”Using hosts File
Section titled “Using hosts File”Map a test domain to localhost:
127.0.0.1 test.homestar.localThen submit requests with that domain:
curl -H "Host: test.homestar.local" \ -X POST \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com", ...}' \ http://localhost:8000/api/contactUsing X-Forwarded-Host
Section titled “Using X-Forwarded-Host”Override the host header directly:
curl -H "X-Forwarded-Host: www.your-domain.com" \ -X POST \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com", ...}' \ http://localhost:8000/api/contactDatabase Schema
Section titled “Database Schema”Brokerage Domains Table
Section titled “Brokerage Domains Table”Domains are kept in a separate table from brokerages so one
brokerage can own apex + www + a branded subdomain:
CREATE TABLE brokerage_domains ( id SERIAL PRIMARY KEY, brokerage_id INTEGER REFERENCES brokerages(id), domain VARCHAR(255) UNIQUE NOT NULL, is_verified BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW());
CREATE INDEX idx_brokerage_domains_domain ON brokerage_domains(domain);is_verified records DNS verification state but does not gate
routing — the row’s existence is enough to say “this domain is
allowed to scope to this brokerage.” This lets you onboard a tenant
the same day you receive the DNS request, without waiting for
propagation.
Leads Table
Section titled “Leads Table”CREATE TABLE leads ( id SERIAL PRIMARY KEY, brokerage_id INTEGER REFERENCES brokerages(id), email VARCHAR(255) NOT NULL, first_name VARCHAR(100), last_name VARCHAR(100), message TEXT, source_page VARCHAR(500), source_domain VARCHAR(255), created_at TIMESTAMPTZ DEFAULT NOW());
CREATE INDEX idx_leads_brokerage_id ON leads(brokerage_id);The brokerage_id foreign key ensures referential integrity and
efficient filtering. source_domain captures the host the lead came
in on — useful when a brokerage runs multiple branded domains and
wants to see which one converted.
Proxy Configuration
Section titled “Proxy Configuration”Caddy automatically forwards the Host header:
your-domain.com { reverse_proxy backend:8000}No additional configuration needed.
Ensure X-Forwarded-Host is set:
location / { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Real-IP $remote_addr;}Multi-Domain Brokerages
Section titled “Multi-Domain Brokerages”A brokerage that wants both kroetchpropertygroup.com and
kroetch.homestar.ink to scope to the same tenant simply gets two
brokerage_domains rows pointing at the same brokerage_id. The
resolver doesn’t care whether a hostname is apex, www, or a HomeStar
subdomain — the lookup is the same.
The www. stripping in _normalize_host means you don’t have to
store both example.com and www.example.com separately; either
one matches.
Related
Section titled “Related”- Leads API Reference — Endpoint specifications including admin-side lead creation
- Brokerages API Reference — Managing tenants and their branding
- Integrating Contact Forms — Frontend implementation