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:

  1. Check if the URL belongs to the admin console (adminUrlSet)
  2. Check if multi-tenancy is enabled and the request uses path-based tenant resolution
  3. 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:

  1. The resolver checks Redis for a cached entry using the hostname
  2. On a cache hit, it returns the tenant ID immediately
  3. On a cache miss, it queries the domains table via findActiveDomain in packages/core/src/queries/domains.ts
  4. 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 getTenantIdFromCustomDomain finds a matching domain that is not active, it returns undefined, causing Logto to fall back to sub-domain pattern matching or return an error
  • Admin console protection: Requests matching adminUrlSet are 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 maxCustomDomains constant in packages/core/src/utils/domain.ts and packages/core/src/constants/index.ts enforces 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 domains table, with active domains routing requests to specific tenant schemas.
  • Resolution occurs in packages/core/src/utils/tenant.ts, where getTenantId checks the admin console, path-based tenancy, and finally custom domain resolution via getTenantIdFromCustomDomain.
  • 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 endpoint to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →