Configuring Custom Domains for Tenant Isolation in Logto
Logto isolates tenants by mapping unique custom domains to specific tenant IDs, extracting the tenant from the request hostname and routing data operations to the isolated tenant schema while caching the mapping in Redis for sub-millisecond resolution.
Logto is an open-source identity and access management platform that supports multi-tenancy through multiple isolation strategies. In the logto-io/logto repository, the custom domain feature allows each tenant to operate under its own branded hostname, ensuring complete data isolation while sharing the same underlying infrastructure. This configuration routes incoming requests to the correct tenant database schema based on the domain name rather than URL paths.
How Custom Domain Tenant Isolation Works
Logto implements tenant isolation through a hostname-to-tenant resolution pipeline that processes every incoming request. When a request arrives on a custom domain, the system extracts the tenant ID from the hostname and routes all subsequent operations to the appropriate tenant data store.
Domain Registration and Storage
Before resolution can occur, an administrator must register the custom domain through Logto’s Management API. The system creates a record in the domains table that stores the tenantId association and an active status flag. Only domains marked as active participate in the tenant resolution process.
Environment Initialization with UrlSet
At startup, Logto builds a UrlSet instance located in packages/shared/src/node/env/UrlSet.ts that holds all allowed URLs for the instance. This set includes the default Logto domain and any custom domains registered in the system. The global EnvSet class (packages/shared/src/node/env/GlobalValues.ts) exposes this urlSet alongside adminUrlSet and feature flags that control multi-tenant behavior.
Request Resolution Logic
The resolution process happens in packages/core/src/utils/tenant.ts within the getTenantId function. This utility receives the incoming request URL and executes the following resolution order:
- Check if the URL belongs to the admin console (
adminUrlSet) - Check if multi-tenancy is enabled and the request uses path-based tenant resolution
- Fall back to custom domain resolution via
getTenantIdFromCustomDomain
const customDomainTenantId = await getTenantIdFromCustomDomain(url, pool);
if (customDomainTenantId) {
return [customDomainTenantId, true];
}
The getTenantIdFromCustomDomain function queries the domains table using createDomainsQueries (specifically findActiveDomain(hostname)), validates the domain is active, and returns the associated tenantId.
The Tenant Resolution Pipeline
The complete resolution pipeline involves database lookups, caching layers, and tenant-aware service routing to ensure both performance and isolation.
Database Queries and Caching
To avoid repeated database lookups, Logto caches the tenant ID for each custom domain. The cache key is constructed by getDomainCacheKey using the format custom-domain:<hostname> and stored in Redis via the redisCache module (packages/core/src/caches/redis.ts).
When a request arrives:
- The resolver checks Redis for a cached entry using the hostname
- On a cache hit, it returns the tenant ID immediately
- On a cache miss, it queries the
domainstable viafindActiveDomaininpackages/core/src/queries/domains.ts - The result is cached before returning to the caller
Tenant-Aware Service Routing
Once the tenant ID is determined, Logto constructs the proper OIDC issuer URL and selects the correct configuration set for that tenant. All subsequent DAO calls route to the tenant-specific database schema, ensuring that user data, authentication settings, and credentials remain isolated from other tenants sharing the same Logto instance.
Edge Cases and Safeguards
Logto handles several edge cases to maintain security and stability:
- Inactive domains: If
getTenantIdFromCustomDomainfinds a matching domain that is not active, it returnsundefined, causing Logto to fall back to sub-domain pattern matching or return an error - Admin console protection: Requests matching
adminUrlSetare recognized early and bypass tenant resolution entirely to protect administrative APIs - Cache invalidation: The
clearCustomDomainCache(url)function purges stale entries when domains are updated or deleted through the Management API - Domain limits: The
maxCustomDomainsconstant inpackages/core/src/utils/domain.tsandpackages/core/src/constants/index.tsenforces limits on custom domains per tenant
Implementation Examples
Registering a Custom Domain via Management API
Create a new custom domain association using the Logto Management API:
POST https://api.logto.io/api/domains
Content-Type: application/json
Authorization: Bearer <admin-access-token>
{
"domain": "login.mycompany.com",
"tenantId": "5d3b0a8c-8f6a-4a5c-9e5f-123456789abc"
}
This endpoint creates a record in the domains table and marks it as active. Logto immediately begins serving requests on that hostname and caches the tenant mapping.
Resolving Tenant ID Internally
When building custom middleware or extensions, use the tenant resolution utilities:
import { getTenantId } from '@logto/core/utils/tenant';
const requestUrl = new URL('https://login.mycompany.com/sign-in');
const [tenantId, isCustom] = await getTenantId(requestUrl);
console.log(tenantId); // → "5d3b0a8c-8f6a-4a5c-9e5f-123456789abc"
console.log(isCustom); // → true
The function returns a tuple where the second element indicates whether the tenant was resolved via custom domain (true) or path-based/default resolution (false).
Configuring the Client SDK
Point Logto client SDKs to the custom domain endpoint:
import { createLogto } from '@logto/client';
const logto = createLogto({
endpoint: 'https://login.mycompany.com',
appId: '<app-id>',
});
await logto.signIn();
The SDK automatically uses the custom domain for all OIDC calls. Logto’s server-side extracts the tenant ID from the hostname, ensuring all token issuance and user profile operations are scoped to that specific tenant.
Deleting a Custom Domain
Remove a domain and clear its cache:
DELETE https://api.logto.io/api/domains/login.mycompany.com
Authorization: Bearer <admin-access-token>
The Management API automatically calls clearCustomDomainCache('login.mycompany.com') to purge the cached mapping, ensuring immediate cessation of routing to that tenant.
Key Architectural Components
| Component | Source File | Role |
|---|---|---|
| UrlSet | packages/shared/src/node/env/UrlSet.ts |
Stores the set of allowed URLs including default and custom domains |
| EnvSet | packages/shared/src/node/env/GlobalValues.ts |
Global configuration exposing urlSet, adminUrlSet, and multi-tenancy feature flags |
| Tenant Resolver | packages/core/src/utils/tenant.ts |
Core logic resolving requests to tenant IDs via custom domains, path-based tenancy, or defaults |
| Domain Utilities | packages/core/src/utils/domain.ts |
Defines maxCustomDomains limits and validation helpers |
| Domain Queries | packages/core/src/queries/domains.ts |
Database layer providing findActiveDomain(hostname) for resolver lookups |
| Redis Cache | packages/core/src/caches/redis.ts |
Caches tenant-domain mappings with custom-domain: prefix keys |
| Constants | packages/core/src/constants/index.ts |
Contains isMultiTenancy and isPathBasedMultiTenancy feature flags |
Summary
- Logto achieves tenant isolation by mapping custom domains to tenant IDs in the
domainstable, with active domains routing requests to specific tenant schemas. - Resolution occurs in
packages/core/src/utils/tenant.ts, wheregetTenantIdchecks the admin console, path-based tenancy, and finally custom domain resolution viagetTenantIdFromCustomDomain. - Redis caching stores mappings under
custom-domain:<hostname>keys to eliminate database lookups on subsequent requests. - The Management API provides endpoints to register, update, and delete custom domains, automatically handling cache invalidation to prevent stale routing.
- Client SDKs require no special configuration beyond pointing the
endpointto the custom domain, as the server handles tenant extraction transparently.
Frequently Asked Questions
What happens if a custom domain is not marked as active?
If the domain record exists but the active flag is false, getTenantIdFromCustomDomain returns undefined, and Logto falls back to sub-domain pattern matching or treats the request as belonging to the default tenant. This prevents routing to tenants before DNS and SSL configuration are complete.
Can one tenant have multiple custom domains?
Yes, the domains table supports multiple entries pointing to the same tenantId. However, the maxCustomDomains constant and related feature flags in packages/core/src/constants/index.ts enforce limits on the number of custom domains per tenant to prevent abuse.
How does Logto handle cache invalidation for custom domains?
When a domain is updated or deleted through the Management API, Logto automatically calls clearCustomDomainCache(url), which constructs the custom-domain:<hostname> key and purges the entry from Redis. This ensures that changes take effect immediately without waiting for TTL expiration.
Is the admin console accessible via custom domains?
No. The getTenantId function checks adminUrlSet early in the resolution pipeline and bypasses tenant resolution for admin console URLs. This isolation protects administrative APIs from being accessed through tenant-specific custom domains, maintaining security boundaries between tenant-facing authentication flows and system administration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →