Skip to main content

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:

  1. Subdomain - acme.booking.com resolves to tenant acme
  2. Custom Domain - booking.acme.com resolves to the tenant with that custom domain configured
  3. Local Development - demo.localhost:3000 resolves to tenant demo

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:

  1. Middleware extracts the host header
  2. Parses the subdomain (acme) from the base domain (booking.com)
  3. Looks up tenant with matching subdomain field
  4. 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 endpoints
  • www - Main website
  • admin - Admin portal
  • app - Application dashboard
  • static - Static assets
  • cdn - 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):

  1. Tenant configures DNS to point their domain to the booking platform
  2. Platform stores the custom domain in tenant record
  3. 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

  1. No /etc/hosts modification required - Modern browsers and curl resolve *.localhost to 127.0.0.1 automatically.

  2. 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
},
});
  1. 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)
  1. Middleware resolves tenant from subdomain/domain
  2. Guard validates tenant exists and is active
  3. 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:

  1. Add subdomains to tenants:
-- Batch update existing tenants
UPDATE "Tenant"
SET subdomain = LOWER(REPLACE(name, ' ', '-'))
WHERE subdomain IS NULL;
  1. Enable subdomain resolution:
TENANT_SUBDOMAIN_RESOLUTION_ENABLED=true
  1. Update DNS (if using custom domains)

  2. Update client applications to use subdomain URLs instead of headers

  3. 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:

  1. Verify subdomain is set:
SELECT id, name, subdomain, custom_domain FROM "Tenant";
  1. Check if subdomain is reserved:
echo $TENANT_RESERVED_SUBDOMAINS
  1. Verify base domain configuration:
echo $TENANT_BASE_DOMAIN
  1. Check if feature is enabled:
echo $TENANT_SUBDOMAIN_RESOLUTION_ENABLED

Caching Issues

Symptoms: Changes to tenant subdomain not taking effect

Solutions:

  1. Wait for cache TTL (default: 5 minutes)
  2. Restart the API server to clear cache
  3. Temporarily disable caching for debugging

Local Development Issues

Symptoms: demo.localhost not resolving

Solutions:

  1. Try with port: demo.localhost:3000
  2. Add to /etc/hosts: 127.0.0.1 demo.localhost
  3. Use a different browser
  4. 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