How Logto Implements Multi-Tenancy with Organizations

Logto implements multi-tenancy by modeling each tenant as a special organization within the admin tenant, using the t-${tenantId} naming convention to reuse existing role-based access control and organization token mechanisms.

Logto is an open-source identity and access management (IAM) platform that handles complex multi-tenant scenarios through a unified organization model. Rather than building separate tenant-specific infrastructure, the codebase leverages existing organization, role, and scope mechanisms to isolate data and manage per-tenant permissions. This architecture, implemented in the logto-io/logto repository, allows the platform to scale horizontally while maintaining strict data isolation through the organizations table in the admin tenant.

Core Concepts of Logto Multi-Tenancy

Tenant Organizations

In Logto, a tenant is represented by a tenant organization—a special entity living in the admin tenant. When a new tenant is created, Logto generates a unique organization ID by prefixing the tenant ID with t- (e.g., t-my-tenant). This convention is defined in packages/schemas/src/types/tenant-organization.ts, which exports the getTenantOrganizationId(tenantId) helper and the frozen CreateOrganization object used during provisioning.

Tenant Scopes and Roles

Access control within tenant organizations relies on two enumerations: TenantScope and TenantRole. The TenantScope enum defines granular actions such as reading tenant data, writing configurations, or managing members. The TenantRole enum maps these scopes to human-readable roles: Admin receives all available scopes, while Collaborator receives a limited subset. This mapping is stored in the tenantRoleScopes relationship, allowing the platform to enforce permissions uniformly across all tenant organizations.

Organization Tokens (RFC 0001)

To authorize requests across tenant boundaries, Logto issues organization tokens—JWTs that carry the organization context via the claim urn:logto:organization:${orgId}. These tokens are requested using the urn:logto:scope:organizations scope and the urn:logto:resource:organizations resource indicator. The token structure is defined in packages/toolkit/core-kit/src/openid.ts and is consumed by the Management API, Cloud APIs, and token-exchange flows to enforce per-organization access policies.

How the Multi-Tenancy Flow Works

Tenant Creation and Seeding

When a tenant is provisioned—either via CLI seed commands or programmatically—Logto calls getTenantOrganizationCreateData(tenantId) to generate the required insertion data. This creates a record in the admin tenant's organizations table with the ID t-${tenantId}.

// packages/cli/src/commands/database/seed/tenant-organizations.ts
import { getTenantOrganizationCreateData } from '@logto/schemas';

const createData = getTenantOrganizationCreateData(tenantId);
await queries.organizations.insert(createData);

First Admin User Provisioning

During the registration of the first admin user, the submit-interaction.ts handler detects the isCreatingFirstAdminUser flag and automatically creates the tenant organization if it does not exist. It then inserts the user into the organizations.relations.users table and assigns the TenantRole.Admin role via organizations.relations.usersRoles.

// packages/core/src/routes/interaction/actions/submit-interaction.ts
const organizationId = getTenantOrganizationId(defaultTenantId);
await organizations.relations.users.insert({ organizationId, userId: id });
await organizations.relations.usersRoles.insert({
  organizationId,
  organizationRoleId: getTenantRole(TenantRole.Admin).id,
  userId: id,
});

Token Issuance via OIDC Grants

When a client requests an access token with the organizations scope, the OIDC grant handlers—including client-credentials.ts, refresh-token.ts, and token-exchange—attach the tenant organization URN to the JWT payload. This allows downstream services to identify the target tenant without additional database lookups.

// packages/core/src/oidc/grants/client-credentials.ts
if (scopes.includes(UserScope.Organizations)) {
  const organizationUrn = buildOrganizationUrn(getTenantOrganizationId(tenantId));
  // Attach to token payload as context.organization
}

Runtime Membership Verification

Every protected endpoint extracts the organization claim from the incoming token and verifies membership against the organizations.relations.users table. If the user is not associated with the specified tenant organization, the API returns a 403 Forbidden response. This check ensures that users can only access data belonging to tenants where they hold active membership and appropriate roles.

Implementation Examples

Creating a Tenant Organization Programmatically

To create a tenant organization via code, use the schema helpers to generate the correct ID and creation data, then insert into the admin tenant's organization table.

import {
  getTenantOrganizationCreateData,
  getTenantOrganizationId,
  getTenantRole,
  TenantRole,
} from '@logto/schemas';

// Generate creation payload for tenant 'acme-corp'
const tenantId = 'acme-corp';
const createData = getTenantOrganizationCreateData(tenantId);

// Insert organization (requires admin tenant database context)
await queries.organizations.insert(createData);

// Assign default admin role
await queries.organizations.relations.roles.insert({
  organizationId: getTenantOrganizationId(tenantId),
  roleId: getTenantRole(TenantRole.Admin).id,
});

Adding Users to a Tenant Organization

New users can be associated with a tenant organization using the relations API.

import { getTenantOrganizationId } from '@logto/schemas';

const organizationId = getTenantOrganizationId(tenantId);
await queries.organizations.relations.users.insert({
  organizationId,
  userId: newUserId,
});

Issuing Organization Tokens

Clients obtain organization tokens through the client-credentials flow by specifying the organizations resource and scope.

import {
  buildOrganizationUrn,
  getTenantOrganizationId,
} from '@logto/schemas';

// Request: grant_type=client_credentials&scope=urn:logto:scope:organizations
//          &resource=urn:logto:resource:organizations

const orgUrn = buildOrganizationUrn(getTenantOrganizationId(tenantId));
const token = await issueJwt({
  sub: clientId,
  scope: 'urn:logto:scope:organizations',
  context: { organization: orgUrn },
});

Verifying Membership in API Endpoints

API routes must validate that the requesting user belongs to the organization specified in the token.

import { getOrganizationIdFromUrn } from '@logto/schemas';

async function handler(ctx) {
  const token = await verifyJwt(ctx.headers.authorization);
  const orgUrn = token.context?.organization;
  
  if (!orgUrn) {
    ctx.throw(401, 'Missing organization token');
  }
  
  const orgId = getOrganizationIdFromUrn(orgUrn);
  const isMember = await queries.organizations.relations.users.findOne({
    organizationId: orgId,
    userId: token.sub,
  });
  
  if (!isMember) {
    ctx.throw(403, 'User not a member of the organization');
  }
  
  // Proceed with tenant-specific business logic
}

Summary

  • Tenant organizations use the t-${tenantId} naming convention to uniquely identify tenants within the admin tenant's organization table, defined in packages/schemas/src/types/tenant-organization.ts.
  • Role-based access control is implemented through TenantScope and TenantRole enums, allowing granular permissions for Admin and Collaborator roles without separate tenant tables.
  • Organization tokens carry the tenant context as a URN (urn:logto:organization:${orgId}) and are issued via standard OIDC grants in packages/core/src/oidc/grants/.
  • Automatic provisioning creates tenant organizations during CLI seeding or first admin registration, handled in packages/core/src/routes/interaction/actions/submit-interaction.ts.
  • Runtime isolation is enforced by verifying user membership against organizations.relations.users before serving protected resources.

Frequently Asked Questions

How does Logto isolate data between tenants?

Logto isolates tenant data by treating each tenant as a distinct organization entity within the admin tenant. Every data access request is scoped to an organization ID formatted as t-${tenantId}, and the platform verifies user membership in the organizations.relations.users table before allowing access. This ensures that users can only interact with resources belonging to organizations where they hold valid roles, effectively creating hard boundaries between tenant datasets.

What is the difference between a standard organization and a tenant organization?

A standard organization represents a business entity or team within a tenant (e.g., a company using your SaaS), while a tenant organization represents the tenant itself within the admin context. Tenant organizations are identified by the t- prefix (e.g., t-default for the default tenant) and are created automatically during tenant provisioning. They utilize the same underlying database schema and relations, but serve as the root container for tenant-wide configuration and administrative access.

How are permissions structured within tenant organizations?

Permissions are structured using the TenantScope enum (defining actions like read:data, write:data, manage:members) and the TenantRole enum (defining Admin and Collaborator roles). Each role maps to a specific subset of scopes in the tenantRoleScopes table. When a user is assigned to a tenant organization, they receive a role that determines their permissible actions, and these claims are embedded in the organization token issued by the OIDC grant handlers.

When is a tenant organization created during user registration?

A tenant organization is created during the registration flow when the isCreatingFirstAdminUser flag is true, indicating this is the initial setup of the Logto instance. The submit-interaction.ts handler invokes getTenantOrganizationCreateData to generate the organization record and immediately assigns the new user to the TenantRole.Admin role. For subsequent users, JIT (Just-In-Time) provisioning via the provisionOrganizations library may create tenant organizations based on email domain policies or manual invitation flows.

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 →