Living Document Notice
Published 2026-09-11. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.
Multi-Tenancy via Host Header Routing
Summary
Traditional multi-tenant hosting architectures rely on static web server configurations, provisioning discrete Nginx virtual host files and executing process reloads whenever a new tenant custom domain or subdomain is registered. On fleets hosting dozens or hundreds of tenant publications, frequent reload signals disrupt keepalive pools, delay provisioning by minutes, and introduce race conditions during DNS validation sweeps.
Harbor routes multi-tenant traffic dynamically at the application layer. Operating behind a single catch-all wildcard reverse proxy, the Hono edge renderer inspects incoming HTTP Host headers, resolves the matching tenant workspace and Directus tenant ID via an in-memory LRU cache, and scopes all subsequent content queries without requiring web server reloads or container restarts.
Routing Topologies: Static VHost Reloads vs Dynamic Header Resolution
Generating separate virtual host blocks for every registered tenant creates operational friction. Nginx configuration reloads (nginx -s reload) fork new worker processes while allowing old workers to drain existing connections. Under sustained inbound traffic, multiple overlapping reloads exhaust system file descriptors and fragment memory buffers.
Dynamic header resolution delegates tenant mapping to the application middleware. Nginx terminates TLS via wildcard certificates (or passes SNI through to an edge termination layer like Cloudflare) and forwards the original Host header to the Harbor renderer on the internal Docker bridge.
| Dimension | Static Nginx VHosts | Dynamic Hono Header Resolution |
|---|---|---|
| New Domain Provisioning | File generation + nginx -t + reload (5s–30s) | Database record insert; active in < 50ms |
| Connection Disruption | Drains worker pools on every config reload | Zero socket drops; persistent upstream keepalives |
| Memory Scaling | Linear growth with server block count in Nginx | Single routing table in application memory |
| Tenant Isolation Model | Port or unix socket per tenant instance | Shared worker runtime; database tenant scoping |
| Custom Domain Handling | Requires automated certbot hook + disk writes | Edge TLS termination + uniform upstream pass |
Hono Tenant Resolution Middleware
The resolution middleware extracts the host, normalizes port designations, checks an internal TTL-bounded lookup cache, and queries Directus only when encountering an unseen hostname.
import { Hono } from "hono";
import { LRUCache } from "lru-cache";
interface TenantContext {
tenantId: string;
subdomain: string;
customDomain: string | null;
status: "active" | "suspended";
}
const tenantCache = new LRUCache<string, TenantContext>({
max: 1000,
ttl: 1000 * 60 * 5 // 5-minute memory cache
});
const app = new Hono();
app.use("*", async (c, next) => {
const rawHost = c.req.header("host") || "";
const hostname = rawHost.split(":")[0].toLowerCase();
let tenant = tenantCache.get(hostname);
if (!tenant) {
tenant = await resolveTenantFromDirectus(hostname);
if (tenant) {
tenantCache.set(hostname, tenant);
}
}
if (!tenant || tenant.status !== "active") {
return c.text("Tenant Not Found or Inactive", 404);
}
c.set("tenant", tenant);
await next();
});
async function resolveTenantFromDirectus(host: string): Promise<TenantContext | null> {
const directusUrl = process.env.DIRECTUS_INTERNAL_URL || "http://127.0.0.1:8055";
const token = process.env.DIRECTUS_READ_TOKEN;
// Filter against either canonical subdomain or verified custom domain
const query = new URLSearchParams({
"filter[_or][0][subdomain][_eq]": host.replace(".bosunpkm.com", ""),
"filter[_or][1][custom_domain][_eq]": host,
"fields": "id,subdomain,custom_domain,status",
"limit": "1"
});
const res = await fetch(`${directusUrl}/items/tenants?${query.toString()}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) return null;
const body = await res.json();
const match = body.data?.[0];
if (!match) return null;
return {
tenantId: match.id,
subdomain: match.subdomain,
customDomain: match.custom_domain,
status: match.status
};
}
app.get("/", (c) => {
const tenant = c.get("tenant") as TenantContext;
return c.html(`<h1>Welcome to ${tenant.subdomain}</h1><p>Tenant ID: ${tenant.tenantId}</p>`);
});
export default app;Immediate Invalidation via Webhook Listener
When an operator updates a domain mapping in Directus, an internal webhook evicts the stale tenant entry from the edge cache without cycling the service.
app.post("/internal/cache/invalidate-tenant", async (c) => {
const secret = c.req.header("x-internal-secret");
if (secret !== process.env.INTERNAL_WEBHOOK_SECRET) {
return c.text("Forbidden", 403);
}
const payload = await c.req.json();
const domain = payload.domain?.toLowerCase();
if (domain && tenantCache.has(domain)) {
tenantCache.delete(domain);
}
return c.json({ evicted: domain, ok: true });
});Perimeter Nginx Catch-All Configuration
Nginx acts as the transport bridge, routing all incoming domains to the Harbor listener without maintaining domain-specific blocks.
server {
listen 80;
listen [::]:80;
server_name _;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Shell Verification and Invalidation Probing
Verify multi-tenant resolution across distinct host headers from the command line:
# Probe default platform subdomain
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: alpha.bosunpkm.com" http://127.0.0.1:3000/
# Probe custom external domain
curl -i -H "Host: docs.customtenant.io" http://127.0.0.1:3000/
# Trigger tenant cache eviction via internal webhook
curl -i -X POST http://127.0.0.1:3000/internal/cache/invalidate-tenant \
-H "x-internal-secret: ${INTERNAL_WEBHOOK_SECRET}" \
-H "Content-Type: application/json" \
-d '{"domain": "docs.customtenant.io"}'- Directus Target: harbor
- Garden Source Reference: [HAR-1002 - Multi-Tenancy via Host Header Routing](HAR-1002 - Multi-Tenancy via Host Header Routing), MOC - Harbor Ecosystem, MOC - Local-First Systems and Synchronization, MOC - Bosun PKM Tools