Tenant Resolution
This guide explains how the booking system automatically resolves tenant context from domain names, enabling white-label deployments where each tenant can use their own subdomain or custom domain.
Overview
Sessiq supports automatic tenant resolution from:
- Subdomain -
acme.booking.comresolves to tenantacme - Custom Domain -
booking.acme.comresolves to the tenant with that custom domain configured - Local Development -
demo.localhost:3000resolves to tenantdemo
This eliminates the need for explicit x-tenant-id headers in production environments, providing a seamless white-label experience.
Resolution Priority
The tenant is resolved in the following order:
1. JWT Token (if authenticated)
↓ (if not present)
2. Subdomain Resolution (e.g., acme.booking.com)
↓ (if no match)
3. Custom Domain Resolution (e.g., booking.acme.com)
↓ (if no match)
4. x-tenant-id Header (fallback)
↓ (if no match)
5. Error: Tenant not found
Subdomain Resolution
How It Works
When a request arrives at acme.booking.com:
- Middleware extracts the host header
- Parses the subdomain (
acme) from the base domain (booking.com) - Looks up tenant with matching
subdomainfield - Sets tenant context for the request
Configuration
Environment Variables:
# Base domain for subdomain extraction
TENANT_BASE_DOMAIN=booking.com
# Reserved subdomains that should not resolve to tenants
TENANT_RESERVED_SUBDOMAINS=api,www,admin,app,static,cdn
# Enable/disable subdomain resolution (useful for gradual rollout)
TENANT_SUBDOMAIN_RESOLUTION_ENABLED=true
Tenant Database Setup:
-- Each tenant needs a unique subdomain
UPDATE "Tenant"
SET subdomain = 'acme'
WHERE id = 't_acme';
Or via API:
curl -X PATCH http://localhost:3001/api/v1/admin/tenants/t_acme \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"subdomain": "acme"}'
Reserved Subdomains
The following subdomains are reserved and will not resolve to tenants:
api- API endpointswww- Main websiteadmin- Admin portalapp- Application dashboardstatic- Static assetscdn- Content delivery
Custom reserved subdomains can be configured via TENANT_RESERVED_SUBDOMAINS.
Custom Domain Resolution
How It Works
For tenants who want to use their own domain (e.g., booking.acme.com):
- Tenant configures DNS to point their domain to the booking platform
- Platform stores the custom domain in tenant record
- Requests to that domain resolve to the configured tenant
Configuration
Tenant Database Setup:
UPDATE "Tenant"
SET custom_domain = 'booking.acme.com'
WHERE id = 't_acme';
DNS Configuration (Tenant's DNS):
booking.acme.com CNAME app.booking.com
Or with an A record pointing to the platform's IP address.
SSL/TLS Certificates
For custom domains to work with HTTPS, you'll need to provision SSL certificates. Options include:
- Let's Encrypt with automatic certificate provisioning
- Cloudflare proxy with automatic SSL
- Manual certificates via your load balancer
Local Development
Using subdomain.localhost
For local development, the system supports *.localhost resolution:
# Access the demo tenant via subdomain
curl http://demo.localhost:3000/api/v1/availability
# Access another tenant
curl http://acme.localhost:3000/api/v1/availability
Setup
-
No
/etc/hostsmodification required - Modern browsers and curl resolve*.localhostto127.0.0.1automatically. -
Ensure subdomain is set in your development seed:
// packages/db/seeds/seed-dev-data.ts
const tenant = await admin.tenant.upsert({
where: { id: 't_demo' },
update: { subdomain: 'demo' },
create: {
id: 't_demo',
name: 'Demo Leisure Activities',
subdomain: 'demo', // For demo.localhost:3000
},
});
- Access your development server:
# API (port 3001)
http://demo.localhost:3001/api/v1/availability
# Web (port 3000)
http://demo.localhost:3000
Browser Support
Most modern browsers support *.localhost:
- Chrome
- Firefox
- Safari
- Edge
If *.localhost doesn't work in your browser, you can add entries to /etc/hosts:
127.0.0.1 demo.localhost
127.0.0.1 acme.localhost
Caching
Tenant lookups are cached in-memory to reduce database load:
// Configuration
TENANT_CACHE_ENABLED = true; // Enable/disable caching
TENANT_CACHE_TTL_MS = 300000; // 5 minutes TTL
Cache Behavior
- Cache Hit: Subdomain/domain → Tenant ID returned immediately
- Cache Miss: Database lookup performed, result cached
- Cache Invalidation: Automatic TTL expiration
- Cache Cleanup: Periodic cleanup when cache exceeds 100 entries
When to Disable Caching
Consider disabling caching when:
- Debugging tenant resolution issues
- Frequently changing tenant configurations
- Running integration tests
Integration with RLS
Tenant resolution works seamlessly with Row-Level Security:
Request → TenantResolverMiddleware → TenantGuard → RLS Context
(sets tenantId) (validates) (enforces)
- Middleware resolves tenant from subdomain/domain
- Guard validates tenant exists and is active
- RLS enforces data isolation at database level
Migration Guide
From Header-Based to Subdomain-Based
If you're migrating from explicit x-tenant-id headers:
- Add subdomains to tenants:
-- Batch update existing tenants
UPDATE "Tenant"
SET subdomain = LOWER(REPLACE(name, ' ', '-'))
WHERE subdomain IS NULL;
- Enable subdomain resolution:
TENANT_SUBDOMAIN_RESOLUTION_ENABLED=true
-
Update DNS (if using custom domains)
-
Update client applications to use subdomain URLs instead of headers
-
Deprecate x-tenant-id header (keep as fallback during transition)
Gradual Rollout
Use the feature flag for gradual rollout:
# Start with disabled (existing behavior)
TENANT_SUBDOMAIN_RESOLUTION_ENABLED=false
# Enable for testing
TENANT_SUBDOMAIN_RESOLUTION_ENABLED=true
Troubleshooting
Tenant Not Resolving
Symptoms: Requests return 400 "Tenant not found"
Checklist:
- Verify subdomain is set:
SELECT id, name, subdomain, custom_domain FROM "Tenant";
- Check if subdomain is reserved:
echo $TENANT_RESERVED_SUBDOMAINS
- Verify base domain configuration:
echo $TENANT_BASE_DOMAIN
- Check if feature is enabled:
echo $TENANT_SUBDOMAIN_RESOLUTION_ENABLED
Caching Issues
Symptoms: Changes to tenant subdomain not taking effect
Solutions:
- Wait for cache TTL (default: 5 minutes)
- Restart the API server to clear cache
- Temporarily disable caching for debugging
Local Development Issues
Symptoms: demo.localhost not resolving
Solutions:
- Try with port:
demo.localhost:3000 - Add to
/etc/hosts:127.0.0.1 demo.localhost - Use a different browser
- Check firewall settings
Security Considerations
Subdomain Hijacking
- All subdomains must be explicitly registered in database
- Reserved subdomains prevent conflicts with system routes
- Case-insensitive matching prevents typosquatting
Custom Domain Verification
Consider implementing domain ownership verification:
- DNS TXT record verification
- HTTP challenge verification
- Email verification to domain admin
Rate Limiting
Tenant resolution lookups are subject to rate limiting to prevent:
- Cache exhaustion attacks
- Database query flooding
- Subdomain enumeration
API Reference
Tenant Resolution Middleware
Module: TenantModule
Middleware: TenantResolverMiddleware
Injection Token: TENANT_CONFIG
interface TenantConfig {
baseDomain: string; // e.g., 'booking.com'
reservedSubdomains: string[]; // e.g., ['api', 'www', 'admin']
cacheEnabled: boolean;
cacheTtlMs: number;
subdomainResolutionEnabled: boolean;
}
Database Schema
model Tenant {
id String @id
name String
subdomain String? @unique // For subdomain resolution
customDomain String? @unique // For custom domain resolution
// ... other fields
}
Next Steps
- Learn about Multi-Tenancy Architecture for RLS details
- Review Authentication for JWT-based tenant context
- See Setup Guide for local development configuration