Subdomain Routing
Route requests to the correct tenant based on subdomains, enabling tenant-specific URLs and seamless multi-tenant access.
Overview
Subdomain routing allows you to:
- Access tenants via unique subdomains (e.g.,
acme.example.com) - Automatically resolve subdomains to tenant IDs
- Reserve subdomains for system use
- Customize subdomain resolution logic
- Support custom domains per tenant
Basic Configuration
Organization-Based Routing
The default configuration uses organizations to resolve subdomains:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
useOrganizations: true, // Default
},
});This maps subdomains to organization names on the main tenant:
| Subdomain | Organization | Tenant |
|---|---|---|
acme.auth.example.com | acme | acme |
widgets.auth.example.com | widgets | widgets |
auth.example.com | none | main |
Reserved Subdomains
Prevent certain subdomains from being used for tenants:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
reservedSubdomains: [
"www",
"api",
"admin",
"app",
"cdn",
"static",
"staging",
"dev",
],
},
});Requests to reserved subdomains are routed to the main tenant.
Custom Resolution
Custom Resolver Function
Provide custom logic for subdomain resolution:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain, context) => {
// Look up tenant by subdomain in database
const tenant = await db.tenants.findBySubdomain(subdomain);
if (tenant) {
return tenant.id;
}
// Try organization lookup as fallback
const org = await db.organizations.findByName(subdomain);
if (org) {
return org.tenant_id;
}
// Return null to use main tenant
return null;
},
},
});Database-Backed Resolution
Store subdomain mappings in the database:
// Add subdomain column to tenants table
interface Tenant {
id: string;
name: string;
subdomain?: string;
custom_domain?: string;
}
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain) => {
// First, try exact subdomain match
const tenant = await db.query(
"SELECT id FROM tenants WHERE subdomain = ?",
[subdomain],
);
if (tenant) {
return tenant.id;
}
// Fallback to organization
const org = await db.query(
"SELECT id FROM organizations WHERE name = ? AND tenant_id = ?",
[subdomain, "main"],
);
return org ? subdomain : null;
},
},
});Routing Flow
How It Works
┌──────────────────────────────────────────────────────────────────┐
│ SUBDOMAIN ROUTING FLOW │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 1. Extract subdomain from request host │
│ └─> Input: "acme.auth.example.com" │
│ └─> Extract: "acme" │
│ │
│ 2. Check if subdomain is reserved │
│ └─> If reserved, use main tenant │
│ └─> Continue to step 3 │
│ │
│ 3. Resolve subdomain to tenant ID │
│ └─> Call resolveSubdomain() if provided │
│ └─> Or lookup organization with matching name │
│ │
│ 4. Set tenant context │
│ └─> Store tenant ID in request context │
│ └─> Make available to downstream handlers │
│ │
│ 5. Load tenant database (if configured) │
│ └─> Call getAdapters(tenantId) │
│ └─> Inject into request context │
│ │
└──────────────────────────────────────────────────────────────────┘Example Flow
Request: https://acme.auth.example.com/api/users
- Extract subdomain:
acme - Check reserved: not reserved
- Resolve: Look up organization
acmeon main tenant → found - Set context:
tenant_id = "acme" - Load database: Get adapters for
acmetenant - Continue: Request proceeds with
acmetenant context
Custom Domains
Supporting Custom Domains
Allow tenants to use their own domains:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain, context) => {
const host = context.req.header("host");
// Check for custom domain first
if (!host?.endsWith("auth.example.com")) {
const tenant = await db.tenants.findByCustomDomain(host);
if (tenant) {
return tenant.id;
}
}
// Fall back to subdomain resolution
return resolveBySubdomain(subdomain);
},
},
});Custom Domain Setup
Store and validate custom domains:
interface TenantWithDomain extends Tenant {
custom_domain?: string;
custom_domain_verified?: boolean;
}
async function setCustomDomain(tenantId: string, domain: string) {
// Validate domain
if (!isValidDomain(domain)) {
throw new Error("Invalid domain format");
}
// Check DNS records
const verified = await verifyDNSRecords(domain);
// Update tenant
await db.tenants.update(tenantId, {
custom_domain: domain,
custom_domain_verified: verified,
});
// Generate SSL certificate if verified
if (verified) {
await generateSSLCertificate(domain);
}
}Middleware Integration
Subdomain Middleware
The subdomain middleware is automatically included:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
},
});
// Middleware is part of multiTenancy.middleware
app.use("*", multiTenancy.middleware);Manual Middleware Setup
Use subdomain middleware separately:
import { createSubdomainMiddleware } from "@authhero/multi-tenancy";
const subdomainMiddleware = createSubdomainMiddleware({
subdomainRouting: {
baseDomain: "auth.example.com",
reservedSubdomains: ["www", "api"],
},
});
app.use("*", subdomainMiddleware);Access Patterns
In Request Handlers
Access the resolved tenant ID:
app.get("/api/users", async (c) => {
// Tenant ID is available in context
const tenantId = c.get("tenant_id");
// Database adapters are already scoped to this tenant
const users = await c.env.data.users.list(tenantId);
return c.json({ users });
});In Hooks
Hooks receive tenant context automatically:
const multiTenancy = setupMultiTenancy({
hooks: {
preUserSignUp: async (user, context) => {
const tenantId = context.tenant_id;
console.log(`User signing up for tenant: ${tenantId}`);
return user;
},
},
});Multi-Level Subdomains
Support Nested Subdomains
Handle multiple subdomain levels:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain, context) => {
const host = context.req.header("host");
const parts = host?.split(".") || [];
// Handle: app.acme.auth.example.com
if (parts.length > 3) {
const [app, tenant] = parts;
// Validate app subdomain
if (["app", "api", "admin"].includes(app)) {
return tenant;
}
}
// Standard: acme.auth.example.com
return subdomain;
},
},
});Environment-Specific Subdomains
Support staging/dev environments:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain, context) => {
const host = context.req.header("host");
// Handle: acme-staging.auth.example.com
if (subdomain.includes("-")) {
const [tenantId, env] = subdomain.split("-");
if (["staging", "dev", "test"].includes(env)) {
// Return tenant with environment context
context.set("environment", env);
return tenantId;
}
}
// Production: acme.auth.example.com
context.set("environment", "production");
return subdomain;
},
},
});Wildcard SSL
Certificate Management
Handle SSL certificates for wildcard domains:
// Cloudflare example
async function setupWildcardSSL(baseDomain: string) {
// Cloudflare automatically handles wildcard SSL
// for domains on their platform
// For custom setup:
await certbot.obtain({
domain: `*.${baseDomain}`,
email: "admin@example.com",
method: "dns",
});
}
// Let's Encrypt with DNS challenge
async function obtainWildcardCert(baseDomain: string) {
return await acme.certificate.create({
domains: [`*.${baseDomain}`, baseDomain],
challenge: "dns-01",
dns: {
provider: "cloudflare",
token: process.env.CLOUDFLARE_API_TOKEN,
},
});
}DNS Configuration
Setup Instructions
Configure DNS for subdomain routing:
; Wildcard A record for IPv4
*.auth.example.com. IN A 203.0.113.1
; Wildcard AAAA record for IPv6
*.auth.example.com. IN AAAA 2001:db8::1
; Or wildcard CNAME to load balancer
*.auth.example.com. IN CNAME lb.example.com.Cloudflare Configuration
Using Cloudflare DNS:
async function setupCloudflare DNS(baseDomain: string) {
const zone = await cloudflare.zones.get(baseDomain);
// Create wildcard DNS record
await cloudflare.dnsRecords.create(zone.id, {
type: "A",
name: `*.auth`,
content: "203.0.113.1",
proxied: true, // Enable Cloudflare proxy
});
}Validation and Security
Validate Subdomains
Ensure subdomains are valid:
function isValidSubdomain(subdomain: string): boolean {
// RFC 1123 subdomain rules
const pattern = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
return pattern.test(subdomain);
}
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain) => {
if (!isValidSubdomain(subdomain)) {
return null; // Use main tenant
}
return await resolveTenant(subdomain);
},
},
});Prevent Subdomain Squatting
Check subdomain availability:
async function isSubdomainAvailable(subdomain: string): Promise<boolean> {
// Check reserved list
if (RESERVED_SUBDOMAINS.includes(subdomain)) {
return false;
}
// Check existing tenants
const existing = await db.tenants.findBySubdomain(subdomain);
if (existing) {
return false;
}
// Check existing organizations
const org = await db.organizations.findByName(subdomain);
if (org) {
return false;
}
return true;
}Rate Limiting
Apply rate limits per subdomain:
import { rateLimiter } from "@/middleware/rate-limit";
app.use("*", async (c, next) => {
const subdomain = extractSubdomain(c.req.header("host"));
// Apply rate limit based on subdomain/tenant
await rateLimiter.check({
key: `tenant:${subdomain}`,
limit: 1000, // requests per hour
window: 3600,
});
await next();
});Best Practices
1. Cache Subdomain Resolution
Cache resolved subdomains to reduce database queries:
const cache = new Map<string, string>();
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
baseDomain: "auth.example.com",
resolveSubdomain: async (subdomain) => {
// Check cache
if (cache.has(subdomain)) {
return cache.get(subdomain)!;
}
// Resolve and cache
const tenantId = await resolveTenant(subdomain);
if (tenantId) {
cache.set(subdomain, tenantId);
}
return tenantId;
},
},
});2. Handle Missing Tenants
Gracefully handle non-existent subdomains:
const multiTenancy = setupMultiTenancy({
subdomainRouting: {
resolveSubdomain: async (subdomain) => {
const tenantId = await resolveTenant(subdomain);
if (!tenantId) {
// Log for monitoring
console.warn(`Unknown subdomain: ${subdomain}`);
}
// Return null to use main tenant as fallback
return tenantId;
},
},
});3. Monitor Subdomain Usage
Track subdomain access patterns:
app.use("*", async (c, next) => {
const subdomain = extractSubdomain(c.req.header("host"));
const tenantId = c.get("tenant_id");
// Track usage
await analytics.track({
event: "subdomain_access",
properties: {
subdomain,
tenant_id: tenantId,
path: c.req.path,
},
});
await next();
});4. Validate During Tenant Creation
Ensure subdomain is valid when creating tenants:
async function createTenant(data: TenantInput) {
// Validate subdomain
if (data.subdomain) {
if (!isValidSubdomain(data.subdomain)) {
throw new Error("Invalid subdomain format");
}
if (!(await isSubdomainAvailable(data.subdomain))) {
throw new Error("Subdomain already taken");
}
}
return await db.tenants.create(data);
}Next Steps
- API Reference - Complete API documentation
- Migration Guide - Migrate from single to multi-tenant
- Architecture - Understanding the organization-tenant model